From db08280f6ef26760ae323a90ad602b714cac2a90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=98=BF=E7=94=B7?= Date: Thu, 30 Apr 2026 14:26:27 +0800 Subject: [PATCH] feat(course-package): add org_visible to course_group and course_package_item table Migrations: - htyws: add org_id, org_visible columns to course_group - htykc: create course_package_item table Backend: - CourseGroup: add org_visible, org_id fields; find_org_visible_by_org_id query - CoursePackageItem: new model with sync/list API - SUPERVISOR role check for package item management - New endpoint: GET /api/v1/ws/find_org_visible_course_groups - New endpoints: POST /api/v1/clazz/course-package/item/sync, GET /api/v1/clazz/course-package/item/list/{package_id} Co-Authored-By: Claude Opus 4.7 --- htykc/src/lib.rs | 4 +- htykc/src/ws_course_package.rs | 85 ++++++++++++++++- .../down.sql | 1 + .../up.sql | 13 +++ htykc_models/src/models.rs | 93 +++++++++++++++++++ htykc_models/src/schema.rs | 13 +++ htyws/src/lib.rs | 4 + htyws/src/ws_all.rs | 40 ++++++++ .../down.sql | 8 ++ .../up.sql | 8 ++ htyws_models/src/models.rs | 27 ++++++ htyws_models/src/schema.rs | 2 + 12 files changed, 296 insertions(+), 2 deletions(-) create mode 100644 htykc_models/migrations/2026-04-30-061901_create_course_package_item/down.sql create mode 100644 htykc_models/migrations/2026-04-30-061901_create_course_package_item/up.sql create mode 100644 htyws_models/migrations/2026-04-30-061900_add_org_id_and_org_visible_to_course_group/down.sql create mode 100644 htyws_models/migrations/2026-04-30-061900_add_org_id_and_org_visible_to_course_group/up.sql diff --git a/htykc/src/lib.rs b/htykc/src/lib.rs index 70c63c4..9c19f1d 100644 --- a/htykc/src/lib.rs +++ b/htykc/src/lib.rs @@ -24,7 +24,7 @@ mod ws_xiaoke; use crate::ws_course_package::{ create_course_package, delete_course_package, find_all_active_course_packages_by_org_with_page, find_all_course_packages_by_created_by_with_page, find_course_package_by_id, - update_course_package, + list_course_package_items, sync_course_package_items, update_course_package, }; use crate::ws_xiaoke::{batch_save_clazz_attendance, find_clazz_attendance_by_clazz_id}; use crate::ws_xiaoke::{ @@ -110,6 +110,8 @@ pub fn clazz_router(db_url: &str) -> Router { .route("/api/v1/clazz/course-package/{id}", get(find_course_package_by_id)) .route("/api/v1/clazz/course-package/my-packages", get(find_all_course_packages_by_created_by_with_page)) .route("/api/v1/clazz/course-package/org-packages", get(find_all_active_course_packages_by_org_with_page)) + .route("/api/v1/clazz/course-package/item/sync", post(sync_course_package_items)) + .route("/api/v1/clazz/course-package/item/list/{package_id}", get(list_course_package_items)) .layer(TraceLayer::new_for_http()) .with_state(shared_db_state); diff --git a/htykc/src/ws_course_package.rs b/htykc/src/ws_course_package.rs index 2ce83d1..6b0b29d 100644 --- a/htykc/src/ws_course_package.rs +++ b/htykc/src/ws_course_package.rs @@ -9,7 +9,7 @@ use htycommons::web::{ wrap_json_anyhow_err, wrap_json_ok_resp, AuthorizationHeader, HtyHostHeader, HtySudoerTokenHeader, }; -use htykc_models::models::{CoursePackage, ReqCoursePackage}; +use htykc_models::models::{CoursePackage, CoursePackageItem, ReqCoursePackage, ReqCoursePackageItem}; use htycommons::uuid; use std::collections::HashMap; use std::ops::DerefMut; @@ -212,3 +212,86 @@ pub async fn find_all_active_course_packages_by_org_with_page( } } } + +// --- 课包内容关联(course_package_item)--- + +fn require_supervisor_role(auth: &AuthorizationHeader) -> anyhow::Result<()> { + let token = jwt_decode_token(&(*auth).clone())?; + let has_role = token.current_org_role_keys + .as_ref() + .map(|keys| keys.iter().any(|k| k == "SUPERVISOR")) + .unwrap_or(false); + if !has_role { + return Err(anyhow!(HtyErr { + code: HtyErrCode::AuthenticationFailed, + reason: Some("需要 SUPERVISOR 角色才能管理机构课包内容".to_string()), + })); + } + Ok(()) +} + +pub async fn sync_course_package_items( + _sudoer: HtySudoerTokenHeader, + auth: AuthorizationHeader, + State(db_pool): State>, + Json(body): Json, +) -> Json> { + let result = (|| -> anyhow::Result { + require_supervisor_role(&auth)?; + let token = jwt_decode_token(&(*auth).clone())?; + let hty_id = token.hty_id.ok_or_else(|| anyhow!("hty_id is required"))?; + let package_id = body.package_id.ok_or_else(|| anyhow!("package_id is required"))?; + let group_ids = body.course_group_ids.clone().ok_or_else(|| anyhow!("course_group_ids is required"))?; + + let mut conn_holder = extract_conn(fetch_db_conn(&db_pool)?); + let conn = conn_holder.deref_mut(); + + // 先软删除现有项目 + CoursePackageItem::logic_delete_by_package_id(&package_id, conn)?; + + // 批量创建新项目 + let now = current_local_datetime(); + let items: Vec = group_ids.iter().enumerate().map(|(i, gid)| { + CoursePackageItem { + id: uuid(), + package_id: package_id.clone(), + course_group_id: gid.clone(), + sort_order: Some(i as i32), + created_at: now, + created_by: Some(hty_id.clone()), + is_delete: Some(false), + } + }).collect(); + + let count = items.len(); + if !items.is_empty() { + CoursePackageItem::batch_create(&items, conn)?; + } + Ok(count) + })(); + match result { + Ok(ok) => wrap_json_ok_resp(ok), + Err(e) => { + error!("sync_course_package_items -> failed, e: {}", e); + wrap_json_anyhow_err(e) + } + } +} + +pub async fn list_course_package_items( + _sudoer: HtySudoerTokenHeader, + _auth: AuthorizationHeader, + State(db_pool): State>, + Path(package_id): Path, +) -> Json>> { + let result = (|| -> anyhow::Result> { + CoursePackageItem::find_by_package_id(&package_id, extract_conn(fetch_db_conn(&db_pool)?).deref_mut()) + })(); + match result { + Ok(ok) => wrap_json_ok_resp(ok), + Err(e) => { + error!("list_course_package_items -> failed, e: {}", e); + wrap_json_anyhow_err(e) + } + } +} diff --git a/htykc_models/migrations/2026-04-30-061901_create_course_package_item/down.sql b/htykc_models/migrations/2026-04-30-061901_create_course_package_item/down.sql new file mode 100644 index 0000000..7d62f04 --- /dev/null +++ b/htykc_models/migrations/2026-04-30-061901_create_course_package_item/down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS course_package_item; diff --git a/htykc_models/migrations/2026-04-30-061901_create_course_package_item/up.sql b/htykc_models/migrations/2026-04-30-061901_create_course_package_item/up.sql new file mode 100644 index 0000000..4fa1fac --- /dev/null +++ b/htykc_models/migrations/2026-04-30-061901_create_course_package_item/up.sql @@ -0,0 +1,13 @@ +CREATE TABLE IF NOT EXISTS course_package_item ( + id VARCHAR NOT NULL PRIMARY KEY, + package_id VARCHAR NOT NULL, + course_group_id VARCHAR NOT NULL, + sort_order INT4 DEFAULT 0, + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + created_by VARCHAR, + is_delete BOOL DEFAULT FALSE +); + +CREATE INDEX IF NOT EXISTS idx_cpi_package_id ON course_package_item (package_id); +CREATE INDEX IF NOT EXISTS idx_cpi_course_group_id ON course_package_item (course_group_id); +CREATE INDEX IF NOT EXISTS idx_cpi_package_course_group ON course_package_item (package_id, course_group_id); diff --git a/htykc_models/src/models.rs b/htykc_models/src/models.rs index e98049b..567699e 100644 --- a/htykc_models/src/models.rs +++ b/htykc_models/src/models.rs @@ -911,6 +911,99 @@ impl CoursePackage { } } +// --- 课包内容关联(course_package_item)--- + +#[derive( + AsChangeset, + Identifiable, + PartialEq, + Serialize, + Deserialize, + Queryable, + Debug, + Insertable, + Clone, +)] +#[diesel(table_name = course_package_item)] +pub struct CoursePackageItem { + pub id: String, + pub package_id: String, + pub course_group_id: String, + pub sort_order: Option, + pub created_at: NaiveDateTime, + pub created_by: Option, + pub is_delete: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct ReqCoursePackageItem { + pub id: Option, + pub package_id: Option, + pub course_group_id: Option, + pub course_group_ids: Option>, + pub sort_order: Option, + pub created_at: Option, + pub created_by: Option, + pub is_delete: Option, +} + +impl CoursePackageItem { + pub fn find_by_package_id( + pkg_id: &str, + conn: &mut PgConnection, + ) -> anyhow::Result> { + use crate::schema::course_package_item::dsl::*; + match course_package_item + .filter(package_id.eq(pkg_id)) + .filter(is_delete.is_null().or(is_delete.eq(false))) + .order(sort_order.asc()) + .load::(conn) + { + Ok(items) => Ok(items), + Err(e) => Err(anyhow!(HtyErr { + code: HtyErrCode::DbErr, + reason: Some(e.to_string()), + })), + } + } + + pub fn batch_create( + items: &[CoursePackageItem], + conn: &mut PgConnection, + ) -> anyhow::Result> { + use crate::schema::course_package_item::dsl::*; + match insert_into(course_package_item) + .values(items) + .get_results(conn) + { + Ok(inserted) => Ok(inserted), + Err(e) => Err(anyhow!(HtyErr { + code: HtyErrCode::DbErr, + reason: Some(e.to_string()), + })), + } + } + + pub fn logic_delete_by_package_id( + pkg_id: &str, + conn: &mut PgConnection, + ) -> anyhow::Result { + use crate::schema::course_package_item::dsl::*; + match diesel::update(course_package_item) + .filter(package_id.eq(pkg_id)) + .filter(is_delete.is_null().or(is_delete.eq(false))) + .set(is_delete.eq(true)) + .execute(conn) + { + Ok(n) => Ok(n), + Err(e) => Err(anyhow!(HtyErr { + code: HtyErrCode::DbErr, + reason: Some(e.to_string()), + })), + } + } +} + // --- 消课 / 课时包(xiaoke)--- #[derive(Queryable, Serialize, Deserialize, Debug, Clone)] diff --git a/htykc_models/src/schema.rs b/htykc_models/src/schema.rs index 16c61a6..82474b5 100644 --- a/htykc_models/src/schema.rs +++ b/htykc_models/src/schema.rs @@ -116,6 +116,18 @@ diesel::table! { } } +diesel::table! { + course_package_item (id) { + id -> Varchar, + package_id -> Varchar, + course_group_id -> Varchar, + sort_order -> Nullable, + created_at -> Timestamp, + created_by -> Nullable, + is_delete -> Nullable, + } +} + diesel::table! { hour_transaction (id) { id -> Varchar, @@ -145,5 +157,6 @@ diesel::allow_tables_to_appear_in_same_query!( clazz_repeat, course_hour_package, course_package, + course_package_item, hour_transaction, ); diff --git a/htyws/src/lib.rs b/htyws/src/lib.rs index 1ff7e18..4ffd5c4 100644 --- a/htyws/src/lib.rs +++ b/htyws/src/lib.rs @@ -191,6 +191,10 @@ pub fn ws_rocket(db_url: &str) -> Router { "/api/v1/ws/delete_course_group/{id_delete}", post(delete_course_group), ) + .route( + "/api/v1/ws/find_org_visible_course_groups", + get(find_org_visible_course_groups), + ) .route("/api/v1/ws/update_piyue", post(update_piyue)) .route("/api/v1/ws/create_piyue2", post(create_piyue2)) .route("/api/v1/ws/create_course", post(create_course)) diff --git a/htyws/src/ws_all.rs b/htyws/src/ws_all.rs index fda0ac9..76099e6 100644 --- a/htyws/src/ws_all.rs +++ b/htyws/src/ws_all.rs @@ -464,6 +464,7 @@ pub fn raw_create_course_group( in_section_group.updated_at = Some(current_local_datetime()); in_section_group.created_by = Some(user_id.clone()); in_section_group.updated_by = Some(user_id); + in_section_group.org_id = token.current_org_id.clone(); let res = CourseGroup::create( &in_section_group, @@ -642,6 +643,45 @@ pub fn raw_delete_course_group( Ok(()) } +pub async fn find_org_visible_course_groups( + _sudoer: HtySudoerTokenHeader, + auth: AuthorizationHeader, + State(db_pool): State>, +) -> Json>> { + debug!("find_org_visible_course_groups -> starts"); + match raw_find_org_visible_course_groups(&auth, db_pool) { + Ok(ok) => { + debug!( + "find_org_visible_course_groups -> success: {:?}!", + ok.len() + ); + wrap_json_ok_resp(ok) + } + Err(e) => { + error!( + "find_org_visible_course_groups -> failed, e: {}", + e + ); + wrap_json_anyhow_err(e) + } + } +} + +pub fn raw_find_org_visible_course_groups( + auth: &AuthorizationHeader, + db_pool: Arc, +) -> anyhow::Result> { + let token = HtyToken::from_jwt(auth.clone().deref())?; + let org = token.current_org_id.ok_or_else(|| anyhow!("org_id is required"))?; + + let groups = CourseGroup::find_org_visible_by_org_id( + &org, + extract_conn(fetch_db_conn(&db_pool)?).deref_mut(), + )?; + + Ok(groups.iter().map(|g| g.to_req()).collect()) +} + pub async fn create_course_section( root: HtySudoerTokenHeader, auth: AuthorizationHeader, diff --git a/htyws_models/migrations/2026-04-30-061900_add_org_id_and_org_visible_to_course_group/down.sql b/htyws_models/migrations/2026-04-30-061900_add_org_id_and_org_visible_to_course_group/down.sql new file mode 100644 index 0000000..891ac52 --- /dev/null +++ b/htyws_models/migrations/2026-04-30-061900_add_org_id_and_org_visible_to_course_group/down.sql @@ -0,0 +1,8 @@ +DROP INDEX IF EXISTS idx_course_group_org_visible; +DROP INDEX IF EXISTS idx_course_group_org_id; + +ALTER TABLE course_group + DROP COLUMN IF EXISTS org_visible; + +ALTER TABLE course_group + DROP COLUMN IF EXISTS org_id; diff --git a/htyws_models/migrations/2026-04-30-061900_add_org_id_and_org_visible_to_course_group/up.sql b/htyws_models/migrations/2026-04-30-061900_add_org_id_and_org_visible_to_course_group/up.sql new file mode 100644 index 0000000..96d2fe3 --- /dev/null +++ b/htyws_models/migrations/2026-04-30-061900_add_org_id_and_org_visible_to_course_group/up.sql @@ -0,0 +1,8 @@ +ALTER TABLE course_group + ADD COLUMN IF NOT EXISTS org_id varchar; + +ALTER TABLE course_group + ADD COLUMN IF NOT EXISTS org_visible boolean DEFAULT false; + +CREATE INDEX IF NOT EXISTS idx_course_group_org_id ON course_group (org_id); +CREATE INDEX IF NOT EXISTS idx_course_group_org_visible ON course_group (org_visible); diff --git a/htyws_models/src/models.rs b/htyws_models/src/models.rs index ba4e697..be27b7a 100644 --- a/htyws_models/src/models.rs +++ b/htyws_models/src/models.rs @@ -546,6 +546,8 @@ pub struct ReqCourseGroup { pub updated_at: Option, pub updated_by: Option, pub teachers: Option>, + pub org_id: Option, + pub org_visible: Option, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] @@ -5239,6 +5241,8 @@ pub struct CourseGroup { pub updated_at: Option, pub updated_by: Option, pub teachers: Option>, + pub org_id: Option, + pub org_visible: Option, } impl CourseGroup { @@ -5270,6 +5274,23 @@ impl CourseGroup { } } + pub fn find_org_visible_by_org_id( + in_org_id: &str, + conn: &mut PgConnection, + ) -> anyhow::Result> { + let q = course_group::table + .filter(course_group::org_id.eq(in_org_id)) + .filter(course_group::org_visible.eq(true)) + .order((course_group::updated_at.desc(), course_group::created_at.desc())); + match q.load::(conn) { + Ok(list) => Ok(list), + Err(e) => Err(anyhow!(HtyErr { + code: HtyErrCode::DbErr, + reason: Some(e.to_string()), + })), + } + } + pub fn create( in_section_group: &CourseGroup, conn: &mut PgConnection, @@ -5332,6 +5353,8 @@ impl CourseGroup { updated_at: self.updated_at.clone(), updated_by: self.updated_by.clone(), teachers: self.teachers.clone(), + org_id: self.org_id.clone(), + org_visible: self.org_visible.clone(), } } @@ -5350,6 +5373,8 @@ impl CourseGroup { updated_at: Some(current_local_datetime()), updated_by: c_in_req.updated_by, teachers: c_in_req.teachers, + org_id: c_in_req.org_id, + org_visible: c_in_req.org_visible, }) } else { Ok(CourseGroup { @@ -5363,6 +5388,8 @@ impl CourseGroup { updated_at: Some(current_local_datetime()), updated_by: c_in_req.updated_by, teachers: c_in_req.teachers, + org_id: c_in_req.org_id, + org_visible: c_in_req.org_visible, }) } } diff --git a/htyws_models/src/schema.rs b/htyws_models/src/schema.rs index 69c72c8..1b56593 100644 --- a/htyws_models/src/schema.rs +++ b/htyws_models/src/schema.rs @@ -69,6 +69,8 @@ diesel::table! { updated_at -> Nullable, updated_by -> Nullable, teachers -> Nullable, + org_id -> Nullable, + org_visible -> Nullable, } }