diff --git a/htykc/logrotate.config b/htykc/logrotate.config new file mode 100644 index 0000000..59cabc7 --- /dev/null +++ b/htykc/logrotate.config @@ -0,0 +1,17 @@ +compress + +"./htykc.log" { + missingok + hourly + rotate 12 + copytruncate + size 200M +} + +"./htykc.nohup.log" { + missingok + hourly + rotate 12 + copytruncate + size 200M +} diff --git a/htykc/src/ws_clazz.rs b/htykc/src/ws_clazz.rs index bf95d5f..015bdff 100644 --- a/htykc/src/ws_clazz.rs +++ b/htykc/src/ws_clazz.rs @@ -741,6 +741,7 @@ async fn raw_create_clazz_with_repeat( is_repeat: in_kecheng.is_repeat.clone(), course_sections: in_kecheng.course_sections.clone(), is_notified: in_kecheng.is_notified.clone(), + completed_at: None, }; debug!( diff --git a/htykc_models/migrations/2026-04-27-100000_add_clazz_completed_at/down.sql b/htykc_models/migrations/2026-04-27-100000_add_clazz_completed_at/down.sql new file mode 100644 index 0000000..55f1d67 --- /dev/null +++ b/htykc_models/migrations/2026-04-27-100000_add_clazz_completed_at/down.sql @@ -0,0 +1 @@ +ALTER TABLE clazz DROP COLUMN IF EXISTS completed_at; diff --git a/htykc_models/migrations/2026-04-27-100000_add_clazz_completed_at/up.sql b/htykc_models/migrations/2026-04-27-100000_add_clazz_completed_at/up.sql new file mode 100644 index 0000000..b3b59fd --- /dev/null +++ b/htykc_models/migrations/2026-04-27-100000_add_clazz_completed_at/up.sql @@ -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 表示业务上未结课。'; diff --git a/htykc_models/src/models.rs b/htykc_models/src/models.rs index 4b83395..7aaa456 100644 --- a/htykc_models/src/models.rs +++ b/htykc_models/src/models.rs @@ -50,6 +50,7 @@ pub struct Clazz { pub is_repeat: Option, pub course_sections: Option>, pub is_notified: Option, + pub completed_at: Option, } // https://weinan.io/2024/02/23/rust-diesel.html @@ -94,6 +95,8 @@ pub struct ReqClazzWithRepeat { pub clz_is_repeat: Option, #[diesel(sql_type = diesel::sql_types::Nullable < diesel::sql_types::Jsonb >)] pub clz_course_sections: Option>, + #[diesel(sql_type = diesel::sql_types::Nullable < diesel::sql_types::Timestamp >)] + pub clz_completed_at: Option, // // 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>> { - 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>> { - 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>, pub course_sections: Option>, pub is_notified: Option, + pub completed_at: Option, } 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::)) + .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) }) } diff --git a/htykc_models/src/schema.rs b/htykc_models/src/schema.rs index 02a319b..d380a30 100644 --- a/htykc_models/src/schema.rs +++ b/htykc_models/src/schema.rs @@ -22,6 +22,7 @@ diesel::table! { is_repeat -> Nullable, course_sections -> Nullable, is_notified -> Nullable, + completed_at -> Nullable, } } diff --git a/htyproc/logrotate.config b/htyproc/logrotate.config new file mode 100644 index 0000000..113c1d5 --- /dev/null +++ b/htyproc/logrotate.config @@ -0,0 +1,17 @@ +compress + +"./htyproc.log" { + missingok + hourly + rotate 12 + copytruncate + size 200M +} + +"./htyproc.nohup.log" { + missingok + hourly + rotate 12 + copytruncate + size 200M +} diff --git a/htyts/logrotate.config b/htyts/logrotate.config new file mode 100644 index 0000000..a53e17c --- /dev/null +++ b/htyts/logrotate.config @@ -0,0 +1,17 @@ +compress + +"./htyts.log" { + missingok + hourly + rotate 12 + copytruncate + size 200M +} + +"./htyts.nohup.log" { + missingok + hourly + rotate 12 + copytruncate + size 200M +} diff --git a/htyws/logrotate.config b/htyws/logrotate.config index 734746b..4d17371 100644 --- a/htyws/logrotate.config +++ b/htyws/logrotate.config @@ -1,9 +1,17 @@ compress "./htyws.log" { + missingok hourly rotate 12 copytruncate size 200M } +"./htyws.nohup.log" { + missingok + hourly + rotate 12 + copytruncate + size 200M +} diff --git a/run_rotate.sh b/run_rotate.sh index 494c93f..01556ef 100755 --- a/run_rotate.sh +++ b/run_rotate.sh @@ -1,12 +1,22 @@ -#!/bin/sh -set -x -HUIWING="/mnt/huiwing/huiwing" -cd $HUIWING/htyuc || exit -/usr/sbin/logrotate -v -s ./logrotate.status ./logrotate.config +#!/usr/bin/env bash +# 与旧机 `su alchemy -c "/mnt/huiwing/huiwing/run_rotate.sh"` 等价:对各 crate 目录下 +# logrotate.config 执行 logrotate(状态文件各目录独立 ./logrotate.status)。 +# 本脚本位于 huike-back 根;默认 monorepo 为上一级(含 AuthCore、huike-back)。 +# 可覆盖:export HUIWING_REPO=/path/to/huiwing-migration +set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO="${HUIWING_REPO:-$(cd "$SCRIPT_DIR/.." && pwd)}" +AUTH="${HUIWING_AUTH:-$REPO/AuthCore}" -cd $HUIWING/htyws || exit -/usr/sbin/logrotate -v -s ./logrotate.status ./logrotate.config +rotate_one() { + local dir="$1" + [ -d "$dir" ] || return 0 + [ -f "$dir/logrotate.config" ] || return 0 + ( cd "$dir" && /usr/sbin/logrotate -s ./logrotate.status ./logrotate.config ) +} -AUTHCORE="/mnt/huiwing/AuthCore" -cd $AUTHCORE/htyuc || exit -/usr/sbin/logrotate -v -s ./logrotate.status ./logrotate.config +rotate_one "$AUTH/htyuc" +rotate_one "$REPO/huike-back/htyws" +rotate_one "$REPO/huike-back/htykc" +rotate_one "$REPO/huike-back/htyts" +rotate_one "$REPO/huike-back/htyproc" diff --git a/scripts/moicen_start_huiwings_stack.sh b/scripts/moicen_start_huiwings_stack.sh index fafeb8a..109166c 100755 --- a/scripts/moicen_start_huiwings_stack.sh +++ b/scripts/moicen_start_huiwings_stack.sh @@ -1,6 +1,7 @@ #!/usr/bin/env bash # 在**新** moicen 裸机:与旧机 alchemy 相同布局(/mnt/.../AuthCore + huike-back), # 在已「cp_envs + pg *_huiwings 数据 + redis 已起 + 已 cargo build --release 可选」后,nohup 起 UC/WS/KC(与现网进程方式一致:在各 crate 子目录 cargo run 以读 .env)。 +# 日志轮转:见仓库 `huike-back/run_rotate.sh` 与 `plan_skills/moicen/moicen-cutover-config-worklog-2026-04-26.md` §8.1(新机 weli crontab 每 5 分钟)。 # 在 huike-back 根以: ./scripts/moicen_start_huiwings_stack.sh # 可设 MOICEN_HUIKE_ROOT / MOICEN_AUTH_ROOT 为绝对路径(默认=本脚本的 ../ 与 ../AuthCore)。 set -euo pipefail