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
+17
View File
@@ -0,0 +1,17 @@
compress
"./htykc.log" {
missingok
hourly
rotate 12
copytruncate
size 200M
}
"./htykc.nohup.log" {
missingok
hourly
rotate 12
copytruncate
size 200M
}
+1
View File
@@ -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!(
@@ -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>,
}
}
+17
View File
@@ -0,0 +1,17 @@
compress
"./htyproc.log" {
missingok
hourly
rotate 12
copytruncate
size 200M
}
"./htyproc.nohup.log" {
missingok
hourly
rotate 12
copytruncate
size 200M
}
+17
View File
@@ -0,0 +1,17 @@
compress
"./htyts.log" {
missingok
hourly
rotate 12
copytruncate
size 200M
}
"./htyts.nohup.log" {
missingok
hourly
rotate 12
copytruncate
size 200M
}
+8
View File
@@ -1,9 +1,17 @@
compress
"./htyws.log" {
missingok
hourly
rotate 12
copytruncate
size 200M
}
"./htyws.nohup.log" {
missingok
hourly
rotate 12
copytruncate
size 200M
}
+20 -10
View File
@@ -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"
+1
View File
@@ -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