Files
huike-back/htykc/src/ws_clazz.rs
T
weli fa14a5ca8c perf: add GIN indexes on clazz JSONB columns and date-range indexes
- GIN indexes on students/teachers (jsonb_path_ops) for fast user lookups
- Composite index on clazz (is_repeat, start_from, end_by) for date range queries
- Indexes on clazz_repeat (clazz_id, repeat_start, repeat_end) for joins

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-03 09:39:36 +08:00

988 lines
31 KiB
Rust

use anyhow::anyhow;
use axum::extract::{Path, Query, State};
use axum::Json;
use diesel::PgConnection;
use htycommons::common::{
current_local_datetime, get_some_from_query_params, string_to_date, string_to_datetime, HtyErr,
HtyErrCode, HtyResponse,
};
use htycommons::db::*;
use htycommons::jwt::jwt_decode_token;
use htycommons::uuid;
use htycommons::web::{
wrap_json_anyhow_err, wrap_json_ok_resp, AuthorizationHeader, HtyHostHeader,
HtySudoerTokenHeader,
};
use htykc_models::models::{
Clazz, ClazzAuditLog, ClazzRepeat, NewClazzAuditLog, ReqClazz, ReqClazzRepeat, ReqClazzWithRepeat,
};
use std::collections::HashMap;
use std::ops::DerefMut;
use std::sync::Arc;
use tracing::{debug, error};
fn current_org_id_from_auth(token: &AuthorizationHeader) -> Option<String> {
jwt_decode_token(&(*token).clone())
.ok()
.and_then(|decoded| decoded.current_org_id)
}
fn hty_id_from_auth(token: &AuthorizationHeader) -> Option<String> {
jwt_decode_token(&(*token).clone())
.ok()
.and_then(|decoded| decoded.hty_id)
}
fn write_clazz_audit_log(
db_pool: &Arc<DbState>,
clazz_id: &str,
action: &str,
operator_hty_id: &str,
changes: Option<serde_json::Value>,
org_id: Option<String>,
) {
if let Ok(conn) = fetch_db_conn(db_pool) {
let _ = ClazzAuditLog::insert(
&NewClazzAuditLog {
id: uuid(),
clazz_id: clazz_id.to_string(),
action: action.to_string(),
operator_hty_id: operator_hty_id.to_string(),
operator_name: None,
changes,
created_at: current_local_datetime(),
org_id,
},
extract_conn(conn).deref_mut(),
);
}
}
pub async fn find_all_non_repeatable_within_date_range(
sudoer: HtySudoerTokenHeader,
host: HtyHostHeader,
auth: AuthorizationHeader,
State(db_pool): State<Arc<DbState>>,
Query(params): Query<HashMap<String, String>>,
) -> Json<HtyResponse<Vec<ReqClazz>>> {
debug!(
"find_all_non_repeatable_within_date_range -> starts, params: {:?}",
params
);
match raw_find_all_non_repeatable_within_date_range(
auth, sudoer, host, db_pool, &params,
) {
Ok(ok) => wrap_json_ok_resp(ok),
Err(e) => {
error!(
"find_all_non_repeatable_within_date_range -> failed to find kecheng, e: {}",
e
);
wrap_json_anyhow_err(e)
}
}
}
fn raw_find_all_non_repeatable_within_date_range(
token: AuthorizationHeader,
_sudoer: HtySudoerTokenHeader,
_host: HtyHostHeader,
db_pool: Arc<DbState>,
params: &HashMap<String, String>,
) -> anyhow::Result<Vec<ReqClazz>> {
let start_from = string_to_date(&get_some_from_query_params::<String>("start_from", &params))?;
let end_by = string_to_date(&get_some_from_query_params::<String>("end_by", &params))?;
debug!(
"raw_find_all_non_repeatable_within_date_range -> start_from: {:?}",
start_from
);
debug!(
"raw_find_all_non_repeatable_within_date_range -> end_by: {:?}",
end_by
);
if start_from.is_none() || end_by.is_none() {
return Err(anyhow!(HtyErr {
code: HtyErrCode::WebErr,
reason: Some("start_from or end_by is none".into()),
}));
}
let some_kechengs = Clazz::find_all_non_repeatable_by_date_range(
start_from
.as_ref()
.ok_or_else(|| anyhow!("start_from is required"))?,
end_by
.as_ref()
.ok_or_else(|| anyhow!("end_by is required"))?,
extract_conn(fetch_db_conn(&db_pool)?).deref_mut(),
)?;
debug!(
"raw_find_all_non_repeatable_within_date_range -> some_kechengs: {:?}",
some_kechengs
);
if let Some(kechengs) = some_kechengs {
let current_org_id = current_org_id_from_auth(&token);
let res: Vec<ReqClazz> = kechengs
.iter()
.filter(|clazz| {
if let Some(org_id) = &current_org_id {
clazz.org_id.as_ref() == Some(org_id)
} else {
true
}
})
.map(|kc| kc.to_req())
.collect();
Ok(res)
} else {
Ok(vec![])
}
}
pub async fn find_all_non_repeatable_within_date_range_by_hty_id(
sudoer: HtySudoerTokenHeader,
host: HtyHostHeader,
auth: AuthorizationHeader,
State(db_pool): State<Arc<DbState>>,
Query(params): Query<HashMap<String, String>>,
) -> Json<HtyResponse<Vec<ReqClazz>>> {
debug!(
"find_all_non_repeatable_within_date_range_by_hty_id -> starts, params: {:?}",
params
);
match raw_find_all_non_repeatable_within_date_range_by_hty_id(
auth, sudoer, host, db_pool, &params,
) {
Ok(ok) => wrap_json_ok_resp(ok),
Err(e) => {
error!(
"find_all_non_repeatable_within_date_range_by_hty_id -> failed to find kecheng, e: {}",
e
);
wrap_json_anyhow_err(e)
}
}
}
fn raw_find_all_non_repeatable_within_date_range_by_hty_id(
token: AuthorizationHeader,
_sudoer: HtySudoerTokenHeader,
_host: HtyHostHeader,
db_pool: Arc<DbState>,
params: &HashMap<String, String>,
) -> anyhow::Result<Vec<ReqClazz>> {
let start_from =
string_to_date(&get_some_from_query_params::<String>("start_from", &params))?;
let end_by = string_to_date(&get_some_from_query_params::<String>("end_by", &params))?;
debug!(
"raw_find_all_non_repeatable_within_date_range_by_hty_id -> start_from: {:?}",
start_from
);
debug!(
"raw_find_all_non_repeatable_within_date_range_by_hty_id -> end_by: {:?}",
end_by
);
let id_user = get_some_from_query_params::<String>("hty_id", &params);
if start_from.is_none() || end_by.is_none() {
return Err(anyhow!(HtyErr {
code: HtyErrCode::WebErr,
reason: Some("start_from or end_by is none".into()),
}));
}
if id_user.is_none() {
return Err(anyhow!(HtyErr {
code: HtyErrCode::WebErr,
reason: Some("hty_id is none".into()),
}));
}
let some_kechengs = Clazz::find_all_non_repeatable_by_date_range_and_user_id(
id_user
.as_ref()
.ok_or_else(|| anyhow!("id_user is required"))?,
start_from
.as_ref()
.ok_or_else(|| anyhow!("start_from is required"))?,
end_by
.as_ref()
.ok_or_else(|| anyhow!("end_by is required"))?,
extract_conn(fetch_db_conn(&db_pool)?).deref_mut(),
)?;
debug!(
"raw_find_all_non_repeatable_within_date_range_by_hty_id -> {:?}",
some_kechengs
);
if let Some(kechengs) = some_kechengs {
let current_org_id = current_org_id_from_auth(&token);
let res: Vec<ReqClazz> = kechengs
.iter()
.filter(|clazz| {
if let Some(org_id) = &current_org_id {
clazz.org_id.as_ref() == Some(org_id)
} else {
true
}
})
.map(|kc| kc.to_req())
.collect();
Ok(res)
} else {
Ok(vec![])
}
}
pub async fn find_all_repeatable_within_date_range(
sudoer: HtySudoerTokenHeader,
host: HtyHostHeader,
auth: AuthorizationHeader,
State(db_pool): State<Arc<DbState>>,
Query(params): Query<HashMap<String, String>>,
) -> Json<HtyResponse<Vec<ReqClazzWithRepeat>>> {
debug!(
"find_all_repeatable_within_date_range -> starts, params: {:?}",
params
);
match raw_find_all_repeatable_within_date_range(auth, sudoer, host, db_pool, &params) {
Ok(kechengs) => wrap_json_ok_resp(kechengs),
Err(e) => {
error!(
"find_all_repeatable_within_date_range -> failed to find kecheng, e: {}",
e
);
wrap_json_anyhow_err(e)
}
}
}
fn raw_find_all_repeatable_within_date_range(
token: AuthorizationHeader,
_sudoer: HtySudoerTokenHeader,
_host: HtyHostHeader,
db_pool: Arc<DbState>,
params: &HashMap<String, String>,
) -> anyhow::Result<Vec<ReqClazzWithRepeat>> {
let start_from = string_to_date(&get_some_from_query_params::<String>("start_from", &params))?;
let end_by = string_to_date(&get_some_from_query_params::<String>("end_by", &params))?;
debug!(
"raw_find_all_repeatable_within_date_range -> start_from: {:?}",
start_from
);
debug!(
"raw_find_all_repeatable_within_date_range -> end_by: {:?}",
end_by
);
if start_from.is_none() || end_by.is_none() {
return Err(anyhow!(HtyErr {
code: HtyErrCode::WebErr,
reason: Some("start_from or end_by is none".into()),
}));
}
let some_kechengs = Clazz::find_all_repeatable_by_date_range(
start_from
.as_ref()
.ok_or_else(|| anyhow!("start_from is required"))?,
end_by
.as_ref()
.ok_or_else(|| anyhow!("end_by is required"))?,
extract_conn(fetch_db_conn(&db_pool)?).deref_mut(),
)?;
debug!(
"raw_find_all_repeatable_within_date_range -> some_kechengs: {:?}",
some_kechengs
);
if let Some(kechengs) = some_kechengs {
let current_org_id = current_org_id_from_auth(&token);
let filtered_kechengs: Vec<ReqClazzWithRepeat> = kechengs
.into_iter()
.filter(|clazz| {
if let Some(org_id) = &current_org_id {
clazz.clz_org_id.as_ref() == Some(org_id)
} else {
true
}
})
.collect();
debug!(
"raw_find_all_repeatable_within_date_range -> kechengs.len(): {:?}",
filtered_kechengs.len()
);
Ok(filtered_kechengs)
} else {
Ok(vec![])
}
}
pub async fn find_all_repeatable_within_date_range_by_hty_id(
sudoer: HtySudoerTokenHeader,
host: HtyHostHeader,
auth: AuthorizationHeader,
State(db_pool): State<Arc<DbState>>,
Query(params): Query<HashMap<String, String>>,
) -> Json<HtyResponse<Vec<ReqClazzWithRepeat>>> {
debug!(
"find_all_repeatable_within_date_range_by_hty_id -> starts, params: {:?}",
params
);
match raw_find_all_repeatable_within_date_range_by_hty_id(
auth, sudoer, host, db_pool, &params,
) {
Ok(ok) => wrap_json_ok_resp(ok),
Err(e) => {
error!(
"find_all_repeatable_within_date_range_by_hty_id -> failed to find kecheng, e: {}",
e
);
wrap_json_anyhow_err(e)
}
}
}
/*
* 找到前端给定时间范围内的所有kecheng数据
* 本周的重复课程如果存在,返回重复课程对应的sudoer课程
* 1. hty_user_id / 哪个用的课程
* 2. start_from / end_by
* 3. 实体kecheng数据
* 4. 需要显示在本周的重复课程的sudoer课程 (kecheng_repeat_status == `OPEN` && is_repeat == True && repeat_end 在本周范围内 or 为空)
* htykc_moicen=# select kecheng.is_repeat from kecheng join kecheng_repeat on kecheng.id = kecheng_repeat.clazz_id where kecheng.is_repeat is not null;
*
* a. 返回的kecheng里,如果`id == root_id`那么此条数据就是sudoer数据.
* b. 如果此条kecheng的`start_from`和`end_by`落在本周,则说明是本周数据
*/
fn raw_find_all_repeatable_within_date_range_by_hty_id(
token: AuthorizationHeader,
_sudoer: HtySudoerTokenHeader,
_host: HtyHostHeader,
db_pool: Arc<DbState>,
params: &HashMap<String, String>,
) -> anyhow::Result<Vec<ReqClazzWithRepeat>> {
let start_from =
string_to_date(&get_some_from_query_params::<String>("start_from", &params))?;
let end_by = string_to_date(&get_some_from_query_params::<String>("end_by", &params))?;
debug!(
"raw_find_all_repeatable_within_date_range_by_hty_id -> start_from: {:?}",
start_from
);
debug!(
"raw_find_all_repeatable_within_date_range_by_hty_id -> end_by: {:?}",
end_by
);
let id_user = get_some_from_query_params::<String>("hty_id", &params);
if start_from.is_none() || end_by.is_none() {
return Err(anyhow!(HtyErr {
code: HtyErrCode::WebErr,
reason: Some("start_from or end_by is none".into()),
}));
}
if id_user.is_none() {
return Err(anyhow!(HtyErr {
code: HtyErrCode::WebErr,
reason: Some("hty_id is none".into()),
}));
}
let some_kechengs = Clazz::find_all_repeatable_by_date_range_and_user_id(
id_user
.as_ref()
.ok_or_else(|| anyhow!("id_user is required"))?,
start_from
.as_ref()
.ok_or_else(|| anyhow!("start_from is required"))?,
end_by
.as_ref()
.ok_or_else(|| anyhow!("end_by is required"))?,
extract_conn(fetch_db_conn(&db_pool)?).deref_mut(),
)?;
debug!(
"raw_find_all_repeatable_within_date_range_by_hty_id -> some_kechengs: {:?}",
some_kechengs
);
if let Some(kechengs) = some_kechengs {
let current_org_id = current_org_id_from_auth(&token);
let filtered_kechengs: Vec<ReqClazzWithRepeat> = kechengs
.into_iter()
.filter(|clazz| {
if let Some(org_id) = &current_org_id {
clazz.clz_org_id.as_ref() == Some(org_id)
} else {
true
}
})
.collect();
debug!(
"raw_find_all_repeatable_within_date_range_by_hty_id -> kechengs.len(): {:?}",
filtered_kechengs.len()
);
Ok(filtered_kechengs)
} else {
Ok(vec![])
}
}
pub async fn find_by_daka_ids(
sudoer: HtySudoerTokenHeader,
host: HtyHostHeader,
auth: AuthorizationHeader,
State(db_pool): State<Arc<DbState>>,
daka_ids: Json<Vec<String>>,
) -> Json<HtyResponse<Vec<ReqClazz>>> {
debug!("find_by_daka_ids -> starts: {:?}", daka_ids);
match raw_find_by_daka_ids(auth, sudoer, host, db_pool, &daka_ids) {
Ok(ok) => wrap_json_ok_resp(ok),
Err(e) => {
error!(
"find_by_daka_ids -> failed to find kecheng, e: {}",
e
);
wrap_json_anyhow_err(e)
}
}
}
fn raw_find_by_daka_ids(
_auth: AuthorizationHeader,
_sudoer: HtySudoerTokenHeader,
_host: HtyHostHeader,
db_pool: Arc<DbState>,
daka_ids: &Vec<String>,
) -> anyhow::Result<Vec<ReqClazz>> {
let db_kechengs = Clazz::find_by_daka_ids(
daka_ids,
extract_conn(fetch_db_conn(&db_pool)?).deref_mut(),
)?;
debug!(
"raw_find_by_daka_ids -> db_kechengs: {:?}",
db_kechengs
);
let req_kechengs = db_kechengs.into_iter().map(|item| item.to_req()).collect();
debug!(
"raw_find_by_daka_ids -> req_kechengs: {:?}",
req_kechengs
);
Ok(req_kechengs)
}
pub async fn find_clazz_repeat_by_id(
sudoer: HtySudoerTokenHeader,
host: HtyHostHeader,
auth: AuthorizationHeader,
State(db_pool): State<Arc<DbState>>,
Path(id): Path<String>,
) -> Json<HtyResponse<ReqClazzRepeat>> {
debug!("find_clazz_repeat_by_id -> starts: {:?}", id);
match raw_find_clazz_repeat_by_id(auth, sudoer, host, db_pool, &id) {
Ok(ok) => wrap_json_ok_resp(ok),
Err(e) => {
error!(
"find_clazz_repeat_by_id -> failed to find kecheng repeat, e: {}",
e
);
wrap_json_anyhow_err(e)
}
}
}
fn raw_find_clazz_repeat_by_id(
_auth: AuthorizationHeader,
_sudoer: HtySudoerTokenHeader,
_host: HtyHostHeader,
db_pool: Arc<DbState>,
kecheng_repeat_id: &String,
) -> anyhow::Result<ReqClazzRepeat> {
let res = ClazzRepeat::find_by_id(
kecheng_repeat_id,
extract_conn(fetch_db_conn(&db_pool)?).deref_mut(),
)?;
if let Some(kecheng_repeat) = res {
return Ok(kecheng_repeat.to_req());
} else {
return Err(anyhow!(HtyErr {
code: HtyErrCode::NullErr,
reason: Some(format!(
"raw_find_clazz_repeat_by_id -> failed to find record according to id :{:?}.",
kecheng_repeat_id
)),
}));
}
}
pub async fn find_clazz_by_hty_id(
sudoer: HtySudoerTokenHeader,
host: HtyHostHeader,
auth: AuthorizationHeader,
State(db_pool): State<Arc<DbState>>,
Query(params): Query<HashMap<String, String>>,
) -> Json<HtyResponse<Vec<ReqClazz>>> {
debug!("find_clazz_by_hty_id -> starts: {:?}", params);
match raw_find_clazz_by_hty_id(auth, sudoer, host, db_pool, &params) {
Ok(ok) => wrap_json_ok_resp(ok),
Err(e) => {
error!(
"find_clazz_by_hty_id -> failed to find kechengs, e: {}",
e
);
wrap_json_anyhow_err(e)
}
}
}
fn raw_find_clazz_by_hty_id(
auth: AuthorizationHeader,
_sudoer: HtySudoerTokenHeader,
_host: HtyHostHeader,
db_pool: Arc<DbState>,
params: &HashMap<String, String>,
) -> anyhow::Result<Vec<ReqClazz>> {
let start_date =
string_to_datetime(&get_some_from_query_params::<String>("start_date", &params))?;
let end_date = string_to_datetime(&get_some_from_query_params::<String>("end_date", &params))?;
debug!(
"raw_find_clazz_by_hty_id -> start_date: {:?}",
start_date
);
debug!("raw_find_clazz_by_hty_id -> end_date: {:?}", end_date);
if start_date.is_none() || end_date.is_none() {
return Err(anyhow!(HtyErr {
code: HtyErrCode::WebErr,
reason: Some("start_date or end_date is none".into()),
}));
}
let id_user = get_some_from_query_params::<String>("hty_id", &params);
if id_user.is_none() {
return Err(anyhow!(HtyErr {
code: HtyErrCode::WebErr,
reason: Some("hty_id is none".into()),
}));
}
let current_org_id = current_org_id_from_auth(&auth);
let kechengs = Clazz::find_by_user_id(
id_user
.as_ref()
.ok_or_else(|| anyhow!("id_user is required"))?,
&start_date,
&end_date,
extract_conn(fetch_db_conn(&db_pool)?).deref_mut(),
)?;
let res = kechengs
.into_iter()
.filter(|clazz| {
if let Some(org_id) = &current_org_id {
clazz.org_id.as_ref() == Some(org_id)
} else {
true
}
})
.map(|item| item.to_req())
.collect();
debug!("raw_find_clazz_by_hty_id -> {:?}", res);
Ok(res)
}
pub async fn update_clazz(
sudoer: HtySudoerTokenHeader,
host: HtyHostHeader,
auth: AuthorizationHeader,
State(db_pool): State<Arc<DbState>>,
Json(in_kecheng): Json<ReqClazz>,
) -> Json<HtyResponse<()>> {
debug!("update_clazz -> in_kecheng: {:?}", in_kecheng);
match raw_update_clazz(auth, sudoer, host, db_pool, &in_kecheng).await {
Ok(ok) => wrap_json_ok_resp(ok),
Err(e) => {
error!("update_clazz -> failed to update clazz, e: {}", e);
wrap_json_anyhow_err(e)
}
}
}
async fn raw_update_clazz(
token: AuthorizationHeader,
_sudoer: HtySudoerTokenHeader,
_host: HtyHostHeader,
db_pool: Arc<DbState>,
in_kecheng: &ReqClazz,
) -> anyhow::Result<()> {
if in_kecheng.id.is_none() {
return Err(anyhow!(HtyErr {
code: HtyErrCode::WebErr,
reason: Some("clazz_id is none".into()),
}));
}
let id_kecheng = in_kecheng
.id
.clone()
.ok_or_else(|| anyhow!("id is required"))?;
let mut db_kecheng = Clazz::find_by_id(
&id_kecheng,
extract_conn(fetch_db_conn(&db_pool)?).deref_mut(),
)?;
let current_org_id = current_org_id_from_auth(&token);
if let Some(org_id) = &current_org_id {
if db_kecheng.org_id.as_ref() != Some(org_id) {
return Err(anyhow!(HtyErr {
code: HtyErrCode::AuthenticationFailed,
reason: Some("clazz does not belong to current organization".to_string()),
}));
}
}
if let Some(clazz_name) = &in_kecheng.clazz_name {
db_kecheng.clazz_name = clazz_name.clone();
}
if let Some(clazz_status) = &in_kecheng.clazz_status {
db_kecheng.clazz_status = clazz_status.clone();
}
if let Some(start_from) = &in_kecheng.start_from {
db_kecheng.start_from = start_from.clone();
}
if let Some(end_by) = &in_kecheng.end_by {
db_kecheng.end_by = end_by.clone();
}
if let Some(root_id) = &in_kecheng.root_id {
db_kecheng.root_id = root_id.clone();
}
if let Some(parent_id) = &in_kecheng.parent_id {
db_kecheng.parent_id = parent_id.clone();
}
db_kecheng.clazz_desc = in_kecheng.clazz_desc.clone();
db_kecheng.duration = in_kecheng.duration.clone();
db_kecheng.clazz_type = in_kecheng.clazz_type.clone();
db_kecheng.students = in_kecheng.students.clone();
db_kecheng.teachers = in_kecheng.teachers.clone();
db_kecheng.jihuas = in_kecheng.jihuas.clone();
db_kecheng.dakas = in_kecheng.dakas.clone();
db_kecheng.is_delete = in_kecheng.is_delete.clone();
db_kecheng.is_repeat = in_kecheng.is_repeat.clone();
db_kecheng.course_sections = in_kecheng.course_sections.clone();
db_kecheng.is_notified = in_kecheng.is_notified.clone();
debug!("raw_update_clazz -> {:?}", db_kecheng);
let _ = Clazz::update(
&db_kecheng,
extract_conn(fetch_db_conn(&db_pool)?).deref_mut(),
)?;
// Audit log
let operator_id = hty_id_from_auth(&token).unwrap_or_default();
let action = if in_kecheng.is_delete == Some(true) { "DELETE" } else { "UPDATE" };
write_clazz_audit_log(
&db_pool,
&id_kecheng,
action,
&operator_id,
Some(serde_json::json!({
"is_delete": in_kecheng.is_delete,
"clazz_name": in_kecheng.clazz_name,
"start_from": in_kecheng.start_from,
"end_by": in_kecheng.end_by,
})),
current_org_id,
);
Ok(())
}
/*async fn find_group_users_by_group_id(_p0: &String, _p1: &HtyHostHeader, _p2: &HtySudoerTokenHeader) -> anyhow::Result<Option<Vec<ReqClazzUser>>> {
unimplemented!()
}
*/
fn raw_create_clazz_with_repeat_tx(
params: HashMap<String, (Clazz, Option<ClazzRepeat>)>,
db_pool: Arc<DbState>,
) -> anyhow::Result<ReqClazz> {
let task = move |in_params: Option<HashMap<String, (Clazz, Option<ClazzRepeat>)>>,
conn: &mut PgConnection|
-> anyhow::Result<ReqClazz> {
let the_params = in_params.ok_or_else(|| anyhow!("params is required"))?;
let (to_create_kecheng, to_create_kecheng_repeat) = the_params
.clone()
.get("params")
.ok_or_else(|| anyhow!("params key not found"))?
.clone();
let created_kecheng = Clazz::create(&to_create_kecheng, conn)?;
debug!(
"raw_create_clazz_with_repeat_tx -> created_kecheng: {:?}",
created_kecheng
);
let mut created_kecheng_repeat = None;
if let Some(kecheng_repeat) = &to_create_kecheng_repeat {
created_kecheng_repeat = Some(ClazzRepeat::create(kecheng_repeat, conn)?);
debug!(
"raw_create_clazz_with_repeat_tx -> created_kecheng_repeat: {:?}",
created_kecheng_repeat
);
}
let out =
ReqClazz::from_clazz_and_repeat(&Some(created_kecheng), &created_kecheng_repeat);
debug!("raw_create_clazz_with_repeat_tx -> out: {:?}", out);
Ok(out)
};
exec_read_write_task(
Box::new(task),
Some(params),
extract_conn(fetch_db_conn(&db_pool)?).deref_mut(),
)
}
pub async fn create_clazz_with_repeat(
sudoer: HtySudoerTokenHeader,
host: HtyHostHeader,
auth: AuthorizationHeader,
State(db_pool): State<Arc<DbState>>,
Json(in_kecheng): Json<ReqClazz>,
) -> Json<HtyResponse<ReqClazz>> {
debug!(
"create_clazz_with_repeat -> in_kecheng: {:?}",
in_kecheng
);
match raw_create_clazz_with_repeat(auth, sudoer, host, db_pool, &in_kecheng).await {
Ok(out) => wrap_json_ok_resp(out),
Err(e) => {
error!(
"create_clazz_with_repeat -> failed to create clazz, e: {}",
e
);
wrap_json_anyhow_err(e)
}
}
}
async fn raw_create_clazz_with_repeat(
token: AuthorizationHeader,
_sudoer: HtySudoerTokenHeader,
_host: HtyHostHeader,
db_pool: Arc<DbState>,
in_kecheng: &ReqClazz,
) -> anyhow::Result<ReqClazz> {
if in_kecheng.clazz_name.is_none()
|| in_kecheng.start_from.is_none()
|| in_kecheng.end_by.is_none()
|| in_kecheng.duration.is_none()
{
return Err(anyhow!(HtyErr {
code: HtyErrCode::WebErr,
reason: Some(
"clazz_name or start_from or end_by or duration or users is none".into(),
),
}));
}
let id_user = jwt_decode_token(&(*token).clone())?
.hty_id
.ok_or_else(|| anyhow!("hty_id is required"))?;
let current_org_id = jwt_decode_token(&(*token).clone())?.current_org_id;
debug!(
"raw_create_clazz_with_repeat -> id_user: {:?}",
id_user
);
let new_clazz_id = uuid();
let id_sudoer = in_kecheng
.root_id
.clone()
.unwrap_or_else(|| new_clazz_id.clone());
let id_parent = in_kecheng
.parent_id
.clone()
.unwrap_or_else(|| new_clazz_id.clone());
let mut to_create_kecheng = Clazz {
id: new_clazz_id.clone(),
clazz_name: in_kecheng
.clazz_name
.clone()
.ok_or_else(|| anyhow!("clazz_name is required"))?,
clazz_status: in_kecheng
.clazz_status
.clone()
.ok_or_else(|| anyhow!("clazz_status is required"))?,
clazz_desc: in_kecheng.clazz_desc.clone(),
start_from: in_kecheng
.start_from
.clone()
.ok_or_else(|| anyhow!("start_from is required"))?,
end_by: in_kecheng
.end_by
.clone()
.ok_or_else(|| anyhow!("end_by is required"))?,
duration: in_kecheng.duration.clone(),
root_id: id_sudoer.clone(),
clazz_type: in_kecheng.clazz_type.clone(),
parent_id: id_parent.clone(),
created_by: in_kecheng.created_by.clone(),
created_at: Some(current_local_datetime()),
students: in_kecheng.students.clone(),
teachers: in_kecheng.teachers.clone(),
jihuas: in_kecheng.jihuas.clone(),
dakas: in_kecheng.dakas.clone(),
is_delete: in_kecheng.is_delete.clone(),
is_repeat: in_kecheng.is_repeat.clone(),
course_sections: in_kecheng.course_sections.clone(),
is_notified: in_kecheng.is_notified.clone(),
completed_at: None,
org_id: in_kecheng.org_id.clone(),
};
if to_create_kecheng.org_id.is_none() {
to_create_kecheng.org_id = current_org_id.clone();
}
debug!(
"raw_create_clazz_with_repeat -> to_create_kecheng: {:?}",
to_create_kecheng
);
let mut to_create_kecheng_repeat: Option<ClazzRepeat> = None;
if let Some(kecheng_repeat) = &in_kecheng.clazz_repeat {
let kecheng_repeat_copy = kecheng_repeat.clone();
to_create_kecheng_repeat = Some(ClazzRepeat {
id: uuid(),
clazz_id: Some(new_clazz_id.clone()),
start_from: kecheng_repeat_copy.start_from.clone(),
end_by: kecheng_repeat_copy.end_by.clone(),
repeat_start: kecheng_repeat_copy.repeat_start.clone(),
repeat_cycle_days: kecheng_repeat_copy.repeat_cycle_days.clone(),
repeat_end: kecheng_repeat_copy.repeat_end.clone(),
repeat_status: kecheng_repeat_copy.repeat_status.clone(),
latest_clazz_created_at: kecheng_repeat_copy.latest_clazz_created_at.clone(),
org_id: kecheng_repeat_copy.org_id.clone(),
})
}
let mut params = HashMap::new();
params.insert(
"params".to_string(),
(to_create_kecheng, to_create_kecheng_repeat),
);
let out_result = raw_create_clazz_with_repeat_tx(params, db_pool.clone());
match out_result {
Ok(out) => {
debug!("created_kecheng: {:?}", out);
write_clazz_audit_log(
&db_pool,
&new_clazz_id,
"CREATE",
&id_user,
Some(serde_json::json!({
"clazz_name": in_kecheng.clazz_name,
"start_from": in_kecheng.start_from,
"end_by": in_kecheng.end_by,
"is_repeat": in_kecheng.is_repeat,
})),
current_org_id.clone(),
);
Ok(out)
}
Err(e) => Err(anyhow!(HtyErr {
code: HtyErrCode::WebErr,
reason: Some("fail to create clazz e: ".to_string() + &e.to_string()),
})),
}
}
pub async fn list_clazz_audit_log(
_sudoer: HtySudoerTokenHeader,
_host: HtyHostHeader,
_auth: AuthorizationHeader,
State(db_pool): State<Arc<DbState>>,
Query(params): Query<HashMap<String, String>>,
) -> Json<HtyResponse<Vec<ClazzAuditLog>>> {
let result = (|| -> anyhow::Result<Vec<ClazzAuditLog>> {
let clazz_id = get_some_from_query_params::<String>("clazz_id", &params)
.ok_or_else(|| anyhow!("clazz_id is required"))?;
ClazzAuditLog::list_by_clazz_id(
&clazz_id,
extract_conn(fetch_db_conn(&db_pool)?).deref_mut(),
)
})();
match result {
Ok(ok) => wrap_json_ok_resp(ok),
Err(e) => {
error!("list_clazz_audit_log -> failed, e: {}", e);
wrap_json_anyhow_err(e)
}
}
}
#[cfg(test)]
mod tests {
use htycommons::common::current_local_datetime;
use htycommons::jwt::jwt_encode_token;
use htycommons::web::{AuthorizationHeader, HtyToken};
use super::current_org_id_from_auth;
fn build_test_auth_header(current_org_id: Option<&str>) -> String {
std::env::set_var("JWT_KEY", "htykc-org-test-key");
let token = HtyToken {
token_id: "test-token-id".to_string(),
hty_id: Some("teacher-1".to_string()),
app_id: Some("app-1".to_string()),
ts: current_local_datetime(),
roles: None,
tags: None,
current_org_id: current_org_id.map(|value| value.to_string()),
current_org_role_keys: Some(vec!["TEACHER".to_string()]),
current_department_id: None,
};
jwt_encode_token(token).expect("encode test token")
}
#[test]
fn should_extract_current_org_id_from_auth_header() {
let auth_header = build_test_auth_header(Some("org-456"));
let parsed_org_id = current_org_id_from_auth(&AuthorizationHeader(auth_header));
assert_eq!(parsed_org_id, Some("org-456".to_string()));
}
#[test]
fn should_return_none_when_auth_header_has_no_org_context() {
let auth_header = build_test_auth_header(None);
let parsed_org_id = current_org_id_from_auth(&AuthorizationHeader(auth_header));
assert_eq!(parsed_org_id, None);
}
}