feat(htykc): clazz completed_at migration; logrotate for htykc/htyproc/htyts

Portable run_rotate; moicen_start_huiwings_stack and ws_clazz updates; schema/models sync.

Made-with: Cursor
This commit is contained in:
2026-04-26 17:14:44 +08:00
parent dcb0d3c365
commit 0e53c9f66d
11 changed files with 118 additions and 12 deletions
@@ -0,0 +1 @@
ALTER TABLE clazz DROP COLUMN IF EXISTS completed_at;
@@ -0,0 +1,7 @@
-- 业务「已结课」:首次点名落库时间;清空出勤后回到 NULL。与 start_from/end_by 日历态互补。
ALTER TABLE clazz
ADD COLUMN completed_at TIMESTAMP NULL;
COMMENT ON COLUMN clazz.completed_at IS
'首次批量保存出勤且存在有效行时写入;出勤被清空则置 NULL。NULL 表示业务上未结课。';
+28 -2
View File
@@ -50,6 +50,7 @@ pub struct Clazz {
pub is_repeat: Option<bool>,
pub course_sections: Option<MultiVals<CourseSectionJsonData>>,
pub is_notified: Option<bool>,
pub completed_at: Option<NaiveDateTime>,
}
// https://weinan.io/2024/02/23/rust-diesel.html
@@ -94,6 +95,8 @@ pub struct ReqClazzWithRepeat {
pub clz_is_repeat: Option<bool>,
#[diesel(sql_type = diesel::sql_types::Nullable < diesel::sql_types::Jsonb >)]
pub clz_course_sections: Option<MultiVals<CourseSectionJsonData>>,
#[diesel(sql_type = diesel::sql_types::Nullable < diesel::sql_types::Timestamp >)]
pub clz_completed_at: Option<NaiveDateTime>,
// // kc_repeat
#[diesel(sql_type = diesel::sql_types::Varchar)]
pub re_id: String,
@@ -144,6 +147,7 @@ impl ReqClazzWithRepeat {
is_repeat: None,
course_sections: None,
is_notified: None,
completed_at: None,
}
};
@@ -183,6 +187,7 @@ impl ReqClazzWithRepeat {
clz_is_delete: the_kecheng.is_delete.clone(),
clz_is_repeat: the_kecheng.is_repeat.clone(),
clz_course_sections: the_kecheng.course_sections.clone(),
clz_completed_at: the_kecheng.completed_at.clone(),
re_id: the_kecheng_repeat.id.clone(),
re_clazz_id: the_kecheng_repeat.clazz_id.clone(),
re_start_from: the_kecheng_repeat.start_from.clone(),
@@ -219,6 +224,7 @@ impl Clazz {
teachers: self.teachers.clone(),
course_sections: self.course_sections.clone(),
is_notified: self.is_notified.clone(),
completed_at: self.completed_at.clone(),
};
req_res
}
@@ -331,7 +337,7 @@ impl Clazz {
end_by: &NaiveDateTime,
conn: &mut PgConnection,
) -> anyhow::Result<Option<Vec<ReqClazzWithRepeat>>> {
let q = format!("SELECT clazz.id AS clz_id, clazz.clazz_name AS clz_clazz_name, clazz.clazz_status AS clz_clazz_status, clazz.clazz_desc AS clz_clazz_desc, clazz.start_from AS clz_start_from, clazz.end_by AS clz_end_by, clazz.duration AS clz_duration, clazz.root_id AS clz_root_id, clazz.clazz_type AS clz_clazz_type, clazz.parent_id AS clz_parent_id, clazz.created_by AS clz_created_by, clazz.created_at AS clz_created_at, clazz.students AS clz_students, clazz.teachers AS clz_teachers, clazz.jihuas AS clz_jihuas, clazz.dakas AS clz_dakas, clazz.is_delete AS clz_is_delete, clazz.is_repeat AS clz_is_repeat, clazz.course_sections AS clz_course_sections, clazz_repeat.id AS re_id, clazz_repeat.clazz_id AS re_clazz_id, clazz_repeat.start_from AS re_start_from, clazz_repeat.end_by AS re_end_by, clazz_repeat.repeat_start AS re_repeat_start, clazz_repeat.repeat_cycle_days AS re_repeat_cycle_days, clazz_repeat.repeat_end AS re_repeat_end, clazz_repeat.repeat_status AS re_repeat_status, clazz_repeat.latest_clazz_created_at AS re_latest_clazz_created_at FROM clazz JOIN clazz_repeat ON clazz.id = clazz_repeat.clazz_id WHERE (clazz.is_repeat IS NOT NULL AND clazz.is_repeat IS TRUE) AND ((clazz_repeat.repeat_start <= '{}' AND clazz_repeat.repeat_end >= '{}') OR (clazz.start_from <= '{}' AND clazz.end_by >= '{}'));", date_to_string(end_by), date_to_string(start_from), date_to_string(end_by), date_to_string(start_from)).to_string();
let q = format!("SELECT clazz.id AS clz_id, clazz.clazz_name AS clz_clazz_name, clazz.clazz_status AS clz_clazz_status, clazz.clazz_desc AS clz_clazz_desc, clazz.start_from AS clz_start_from, clazz.end_by AS clz_end_by, clazz.duration AS clz_duration, clazz.root_id AS clz_root_id, clazz.clazz_type AS clz_clazz_type, clazz.parent_id AS clz_parent_id, clazz.created_by AS clz_created_by, clazz.created_at AS clz_created_at, clazz.students AS clz_students, clazz.teachers AS clz_teachers, clazz.jihuas AS clz_jihuas, clazz.dakas AS clz_dakas, clazz.is_delete AS clz_is_delete, clazz.is_repeat AS clz_is_repeat, clazz.course_sections AS clz_course_sections, clazz.completed_at AS clz_completed_at, clazz_repeat.id AS re_id, clazz_repeat.clazz_id AS re_clazz_id, clazz_repeat.start_from AS re_start_from, clazz_repeat.end_by AS re_end_by, clazz_repeat.repeat_start AS re_repeat_start, clazz_repeat.repeat_cycle_days AS re_repeat_cycle_days, clazz_repeat.repeat_end AS re_repeat_end, clazz_repeat.repeat_status AS re_repeat_status, clazz_repeat.latest_clazz_created_at AS re_latest_clazz_created_at FROM clazz JOIN clazz_repeat ON clazz.id = clazz_repeat.clazz_id WHERE (clazz.is_repeat IS NOT NULL AND clazz.is_repeat IS TRUE) AND ((clazz_repeat.repeat_start <= '{}' AND clazz_repeat.repeat_end >= '{}') OR (clazz.start_from <= '{}' AND clazz.end_by >= '{}'));", date_to_string(end_by), date_to_string(start_from), date_to_string(end_by), date_to_string(start_from)).to_string();
debug!("find_all_repeatable_by_date_range -> q: {:?}", q);
@@ -352,7 +358,7 @@ impl Clazz {
end_by: &NaiveDateTime,
conn: &mut PgConnection,
) -> anyhow::Result<Option<Vec<ReqClazzWithRepeat>>> {
let q = format!("SELECT clazz.id AS clz_id, clazz.clazz_name AS clz_clazz_name, clazz.clazz_status AS clz_clazz_status, clazz.clazz_desc AS clz_clazz_desc, clazz.start_from AS clz_start_from, clazz.end_by AS clz_end_by, clazz.duration AS clz_duration, clazz.root_id AS clz_root_id, clazz.clazz_type AS clz_clazz_type, clazz.parent_id AS clz_parent_id, clazz.created_by AS clz_created_by, clazz.created_at AS clz_created_at, clazz.students AS clz_students, clazz.teachers AS clz_teachers, clazz.jihuas AS clz_jihuas, clazz.dakas AS clz_dakas, clazz.is_delete AS clz_is_delete, clazz.is_repeat AS clz_is_repeat, clazz.course_sections AS clz_course_sections, clazz_repeat.id AS re_id, clazz_repeat.clazz_id AS re_clazz_id, clazz_repeat.start_from AS re_start_from, clazz_repeat.end_by AS re_end_by, clazz_repeat.repeat_start AS re_repeat_start, clazz_repeat.repeat_cycle_days AS re_repeat_cycle_days, clazz_repeat.repeat_end AS re_repeat_end, clazz_repeat.repeat_status AS re_repeat_status, clazz_repeat.latest_clazz_created_at AS re_latest_clazz_created_at FROM clazz JOIN clazz_repeat ON clazz.id = clazz_repeat.clazz_id WHERE (jsonb_path_exists(clazz.students, '$.val.users.vals[*].user_id ? (@ == \"{}\")') OR jsonb_path_exists(clazz.teachers, '$.val.users.vals[*].user_id ? (@ == \"{}\")')) AND (clazz.is_repeat IS NOT NULL AND clazz.is_repeat IS TRUE) AND ((clazz_repeat.repeat_start <= '{}' AND clazz_repeat.repeat_end >= '{}') OR (clazz.start_from <= '{}' AND clazz.end_by >= '{}'));", id_user.clone(), id_user.clone(), date_to_string(end_by), date_to_string(start_from), date_to_string(end_by), date_to_string(start_from)).to_string();
let q = format!("SELECT clazz.id AS clz_id, clazz.clazz_name AS clz_clazz_name, clazz.clazz_status AS clz_clazz_status, clazz.clazz_desc AS clz_clazz_desc, clazz.start_from AS clz_start_from, clazz.end_by AS clz_end_by, clazz.duration AS clz_duration, clazz.root_id AS clz_root_id, clazz.clazz_type AS clz_clazz_type, clazz.parent_id AS clz_parent_id, clazz.created_by AS clz_created_by, clazz.created_at AS clz_created_at, clazz.students AS clz_students, clazz.teachers AS clz_teachers, clazz.jihuas AS clz_jihuas, clazz.dakas AS clz_dakas, clazz.is_delete AS clz_is_delete, clazz.is_repeat AS clz_is_repeat, clazz.course_sections AS clz_course_sections, clazz.completed_at AS clz_completed_at, clazz_repeat.id AS re_id, clazz_repeat.clazz_id AS re_clazz_id, clazz_repeat.start_from AS re_start_from, clazz_repeat.end_by AS re_end_by, clazz_repeat.repeat_start AS re_repeat_start, clazz_repeat.repeat_cycle_days AS re_repeat_cycle_days, clazz_repeat.repeat_end AS re_repeat_end, clazz_repeat.repeat_status AS re_repeat_status, clazz_repeat.latest_clazz_created_at AS re_latest_clazz_created_at FROM clazz JOIN clazz_repeat ON clazz.id = clazz_repeat.clazz_id WHERE (jsonb_path_exists(clazz.students, '$.val.users.vals[*].user_id ? (@ == \"{}\")') OR jsonb_path_exists(clazz.teachers, '$.val.users.vals[*].user_id ? (@ == \"{}\")')) AND (clazz.is_repeat IS NOT NULL AND clazz.is_repeat IS TRUE) AND ((clazz_repeat.repeat_start <= '{}' AND clazz_repeat.repeat_end >= '{}') OR (clazz.start_from <= '{}' AND clazz.end_by >= '{}'));", id_user.clone(), id_user.clone(), date_to_string(end_by), date_to_string(start_from), date_to_string(end_by), date_to_string(start_from)).to_string();
debug!(
"find_all_repeatable_by_date_range_and_user_id -> q: {:?}",
@@ -602,6 +608,7 @@ pub struct ReqClazz {
pub teachers: Option<SingleVal<ReqHtyUserGroup>>,
pub course_sections: Option<MultiVals<CourseSectionJsonData>>,
pub is_notified: Option<bool>,
pub completed_at: Option<NaiveDateTime>,
}
impl ReqClazz {
@@ -633,6 +640,7 @@ impl ReqClazz {
is_repeat: None,
course_sections: None,
is_notified: None,
completed_at: None,
}
};
@@ -668,6 +676,7 @@ impl ReqClazz {
teachers: the_kecheng.teachers,
course_sections: the_kecheng.course_sections,
is_notified: the_kecheng.is_notified,
completed_at: the_kecheng.completed_at,
}
}
}
@@ -822,6 +831,23 @@ impl ClazzAttendance {
insert_into(clazz_attendance).values(&rows).execute(conn)?;
}
if items.is_empty() {
diesel::update(
crate::schema::clazz::table
.filter(crate::schema::clazz::id.eq(clazz_id_in)),
)
.set(crate::schema::clazz::completed_at.eq(None::<NaiveDateTime>))
.execute(conn)?;
} else {
diesel::update(
crate::schema::clazz::table
.filter(crate::schema::clazz::id.eq(clazz_id_in))
.filter(crate::schema::clazz::completed_at.is_null()),
)
.set(crate::schema::clazz::completed_at.eq(Some(now)))
.execute(conn)?;
}
ClazzAttendance::find_all_active_by_clazz_id(clazz_id_in, conn)
})
}
+1
View File
@@ -22,6 +22,7 @@ diesel::table! {
is_repeat -> Nullable<Bool>,
course_sections -> Nullable<Jsonb>,
is_notified -> Nullable<Bool>,
completed_at -> Nullable<Timestamp>,
}
}