mirror of
https://github.com/SECTL/SecScore.git
synced 2026-07-19 19:04:23 +08:00
feat: 新增学生小组设置与主页小组排序
This commit is contained in:
@@ -95,6 +95,7 @@ pub enum ConflictStrategy {
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
struct StudentNormalized {
|
||||
name: String,
|
||||
group_name: Option<String>,
|
||||
score: i32,
|
||||
reward_points: i32,
|
||||
tags: String,
|
||||
@@ -198,6 +199,7 @@ async fn load_students(
|
||||
row.name.clone(),
|
||||
StudentNormalized {
|
||||
name: row.name,
|
||||
group_name: row.group_name,
|
||||
score: row.score,
|
||||
reward_points: row.reward_points,
|
||||
tags: normalize_tags(&row.tags),
|
||||
@@ -403,6 +405,7 @@ async fn upsert_student(
|
||||
Some(row) => {
|
||||
let normalized_current = StudentNormalized {
|
||||
name: row.name.clone(),
|
||||
group_name: row.group_name.clone(),
|
||||
score: row.score,
|
||||
reward_points: row.reward_points,
|
||||
tags: normalize_tags(&row.tags),
|
||||
@@ -415,6 +418,7 @@ async fn upsert_student(
|
||||
}
|
||||
let mut active: students::ActiveModel = row.into();
|
||||
active.score = Set(data.score);
|
||||
active.group_name = Set(data.group_name.clone());
|
||||
active.reward_points = Set(data.reward_points);
|
||||
active.tags = Set(data.tags.clone());
|
||||
active.extra_json = Set(data.extra_json.clone());
|
||||
@@ -427,6 +431,7 @@ async fn upsert_student(
|
||||
students::ActiveModel {
|
||||
id: sea_orm::ActiveValue::NotSet,
|
||||
name: Set(data.name.clone()),
|
||||
group_name: Set(data.group_name.clone()),
|
||||
score: Set(data.score),
|
||||
reward_points: Set(data.reward_points),
|
||||
tags: Set(data.tags.clone()),
|
||||
|
||||
@@ -17,6 +17,7 @@ use super::response::{ImportResult, IpcResponse};
|
||||
#[derive(Deserialize)]
|
||||
pub struct CreateStudentData {
|
||||
pub name: String,
|
||||
pub group_name: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -64,6 +65,7 @@ pub async fn student_query(
|
||||
.map(|s| StudentWithTags {
|
||||
id: s.id,
|
||||
name: s.name,
|
||||
group_name: s.group_name,
|
||||
score: s.score,
|
||||
reward_points: s.reward_points,
|
||||
tags: serde_json::from_str(&s.tags).unwrap_or_default(),
|
||||
@@ -117,9 +119,16 @@ pub async fn student_create(
|
||||
let now = chrono::Utc::now()
|
||||
.format("%Y-%m-%dT%H:%M:%S%.3fZ")
|
||||
.to_string();
|
||||
let group_name = data
|
||||
.group_name
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|v| !v.is_empty())
|
||||
.map(|v| v.to_string());
|
||||
let new_student = students::ActiveModel {
|
||||
id: sea_orm::ActiveValue::NotSet,
|
||||
name: Set(name.to_string()),
|
||||
group_name: Set(group_name),
|
||||
score: Set(0),
|
||||
reward_points: Set(0),
|
||||
tags: Set("[]".to_string()),
|
||||
@@ -175,6 +184,14 @@ pub async fn student_update(
|
||||
if let Some(name) = data.name {
|
||||
active.name = Set(name);
|
||||
}
|
||||
if let Some(group_name) = data.group_name {
|
||||
let normalized = group_name.trim();
|
||||
active.group_name = if normalized.is_empty() {
|
||||
Set(None)
|
||||
} else {
|
||||
Set(Some(normalized.to_string()))
|
||||
};
|
||||
}
|
||||
if let Some(score) = data.score {
|
||||
active.score = Set(score);
|
||||
}
|
||||
@@ -231,6 +248,7 @@ pub async fn student_delete(
|
||||
students::Entity::delete(students::ActiveModel {
|
||||
id: sea_orm::ActiveValue::Set(student.id),
|
||||
name: sea_orm::ActiveValue::Unchanged(student.name),
|
||||
group_name: sea_orm::ActiveValue::Unchanged(student.group_name),
|
||||
score: sea_orm::ActiveValue::Unchanged(student.score),
|
||||
reward_points: sea_orm::ActiveValue::Unchanged(student.reward_points),
|
||||
tags: sea_orm::ActiveValue::Unchanged(student.tags),
|
||||
@@ -317,6 +335,7 @@ pub async fn student_import_from_xlsx(
|
||||
let new_student = students::ActiveModel {
|
||||
id: sea_orm::ActiveValue::NotSet,
|
||||
name: Set(name.to_string()),
|
||||
group_name: Set(None),
|
||||
score: Set(0),
|
||||
reward_points: Set(0),
|
||||
tags: Set("[]".to_string()),
|
||||
|
||||
@@ -7,6 +7,7 @@ pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i32,
|
||||
pub name: String,
|
||||
pub group_name: Option<String>,
|
||||
pub score: i32,
|
||||
pub reward_points: i32,
|
||||
pub tags: String,
|
||||
|
||||
@@ -23,6 +23,7 @@ impl Migration {
|
||||
Self::create_reward_settings_table(conn, is_sqlite).await?;
|
||||
Self::create_reward_redemptions_table(conn, is_sqlite).await?;
|
||||
Self::ensure_students_reward_points_column(conn, is_sqlite).await?;
|
||||
Self::ensure_students_group_name_column(conn, is_sqlite).await?;
|
||||
|
||||
Self::create_indexes(conn, is_sqlite).await?;
|
||||
|
||||
@@ -163,6 +164,39 @@ impl Migration {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn ensure_students_group_name_column(
|
||||
conn: &impl ConnectionTrait,
|
||||
sqlite: bool,
|
||||
) -> Result<(), DbErr> {
|
||||
let db_backend = Self::get_db_backend(sqlite);
|
||||
let alter_sql = "ALTER TABLE students ADD COLUMN group_name TEXT";
|
||||
let result = conn
|
||||
.execute(Statement::from_string(
|
||||
db_backend.clone(),
|
||||
alter_sql.to_string(),
|
||||
))
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(_) => {
|
||||
info!("Added students.group_name column");
|
||||
}
|
||||
Err(e) => {
|
||||
let msg = e.to_string().to_lowercase();
|
||||
let already_exists = msg.contains("duplicate column")
|
||||
|| msg.contains("already exists")
|
||||
|| msg.contains("duplicate");
|
||||
if already_exists {
|
||||
info!("students.group_name already exists, skip alter");
|
||||
} else {
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn create_indexes(conn: &impl ConnectionTrait, sqlite: bool) -> Result<(), DbErr> {
|
||||
let indexes = vec![
|
||||
get_create_index_score_events_settlement_id_sql(sqlite),
|
||||
|
||||
@@ -30,7 +30,7 @@ impl StudentRepository {
|
||||
pub async fn find_all(&self) -> Result<Vec<StudentWithTags>, sqlx::Error> {
|
||||
if let Some(pool) = &self.sqlite_pool {
|
||||
let students = sqlx::query_as::<Sqlite, Student>(
|
||||
r#"SELECT id, name, score, reward_points, tags, extra_json, created_at, updated_at
|
||||
r#"SELECT id, name, group_name, score, reward_points, tags, extra_json, created_at, updated_at
|
||||
FROM students
|
||||
ORDER BY score DESC, name ASC"#
|
||||
)
|
||||
@@ -40,7 +40,7 @@ impl StudentRepository {
|
||||
Ok(students.into_iter().map(StudentWithTags::from).collect())
|
||||
} else if let Some(pool) = &self.postgres_pool {
|
||||
let students = sqlx::query_as::<Postgres, Student>(
|
||||
r#"SELECT id, name, score, reward_points, tags, extra_json, created_at, updated_at
|
||||
r#"SELECT id, name, group_name, score, reward_points, tags, extra_json, created_at, updated_at
|
||||
FROM students
|
||||
ORDER BY score DESC, name ASC"#
|
||||
)
|
||||
@@ -59,8 +59,8 @@ impl StudentRepository {
|
||||
|
||||
if let Some(pool) = &self.sqlite_pool {
|
||||
let result = sqlx::query(
|
||||
r#"INSERT INTO students (name, score, reward_points, tags, extra_json, created_at, updated_at)
|
||||
VALUES (?, 0, 0, '[]', NULL, ?, ?)"#
|
||||
r#"INSERT INTO students (name, group_name, score, reward_points, tags, extra_json, created_at, updated_at)
|
||||
VALUES (?, NULL, 0, 0, '[]', NULL, ?, ?)"#
|
||||
)
|
||||
.bind(name)
|
||||
.bind(&now)
|
||||
@@ -71,8 +71,8 @@ impl StudentRepository {
|
||||
Ok(result.last_insert_rowid() as i32)
|
||||
} else if let Some(pool) = &self.postgres_pool {
|
||||
let result = sqlx::query_scalar::<Postgres, i32>(
|
||||
r#"INSERT INTO students (name, score, reward_points, tags, extra_json, created_at, updated_at)
|
||||
VALUES ($1, 0, 0, '[]', NULL, $2, $3)
|
||||
r#"INSERT INTO students (name, group_name, score, reward_points, tags, extra_json, created_at, updated_at)
|
||||
VALUES ($1, NULL, 0, 0, '[]', NULL, $2, $3)
|
||||
RETURNING id"#
|
||||
)
|
||||
.bind(name)
|
||||
@@ -98,6 +98,15 @@ impl StudentRepository {
|
||||
query.push(", name = ");
|
||||
query.push_bind(name);
|
||||
}
|
||||
if let Some(group_name) = &data.group_name {
|
||||
let normalized = group_name.trim();
|
||||
query.push(", group_name = ");
|
||||
if normalized.is_empty() {
|
||||
query.push("NULL");
|
||||
} else {
|
||||
query.push_bind(normalized.to_string());
|
||||
}
|
||||
}
|
||||
if let Some(score) = data.score {
|
||||
query.push(", score = ");
|
||||
query.push_bind(score);
|
||||
@@ -127,6 +136,15 @@ impl StudentRepository {
|
||||
query.push(", name = ");
|
||||
query.push_bind(name);
|
||||
}
|
||||
if let Some(group_name) = &data.group_name {
|
||||
let normalized = group_name.trim();
|
||||
query.push(", group_name = ");
|
||||
if normalized.is_empty() {
|
||||
query.push("NULL");
|
||||
} else {
|
||||
query.push_bind(normalized.to_string());
|
||||
}
|
||||
}
|
||||
if let Some(score) = data.score {
|
||||
query.push(", score = ");
|
||||
query.push_bind(score);
|
||||
@@ -156,7 +174,7 @@ impl StudentRepository {
|
||||
pub async fn delete(&self, id: i32) -> Result<(), sqlx::Error> {
|
||||
if let Some(pool) = &self.sqlite_pool {
|
||||
let student = sqlx::query_as::<Sqlite, Student>(
|
||||
"SELECT id, name, score, reward_points, tags, extra_json, created_at, updated_at FROM students WHERE id = ?"
|
||||
"SELECT id, name, group_name, score, reward_points, tags, extra_json, created_at, updated_at FROM students WHERE id = ?"
|
||||
)
|
||||
.bind(id)
|
||||
.fetch_optional(pool)
|
||||
@@ -179,7 +197,7 @@ impl StudentRepository {
|
||||
}
|
||||
} else if let Some(pool) = &self.postgres_pool {
|
||||
let student = sqlx::query_as::<Postgres, Student>(
|
||||
"SELECT id, name, score, reward_points, tags, extra_json, created_at, updated_at FROM students WHERE id = $1"
|
||||
"SELECT id, name, group_name, score, reward_points, tags, extra_json, created_at, updated_at FROM students WHERE id = $1"
|
||||
)
|
||||
.bind(id)
|
||||
.fetch_optional(pool)
|
||||
@@ -247,7 +265,7 @@ impl StudentRepository {
|
||||
|
||||
for name in &to_insert {
|
||||
sqlx::query(
|
||||
"INSERT INTO students (name, score, reward_points, tags, extra_json, created_at, updated_at) VALUES (?, 0, 0, '[]', NULL, ?, ?)"
|
||||
"INSERT INTO students (name, group_name, score, reward_points, tags, extra_json, created_at, updated_at) VALUES (?, NULL, 0, 0, '[]', NULL, ?, ?)"
|
||||
)
|
||||
.bind(name)
|
||||
.bind(&now)
|
||||
@@ -282,7 +300,7 @@ impl StudentRepository {
|
||||
|
||||
for name in &to_insert {
|
||||
sqlx::query(
|
||||
"INSERT INTO students (name, score, reward_points, tags, extra_json, created_at, updated_at) VALUES ($1, 0, 0, '[]', NULL, $2, $3)"
|
||||
"INSERT INTO students (name, group_name, score, reward_points, tags, extra_json, created_at, updated_at) VALUES ($1, NULL, 0, 0, '[]', NULL, $2, $3)"
|
||||
)
|
||||
.bind(name)
|
||||
.bind(&now)
|
||||
@@ -306,14 +324,14 @@ impl StudentRepository {
|
||||
pub async fn find_by_name(&self, name: &str) -> Result<Option<Student>, sqlx::Error> {
|
||||
if let Some(pool) = &self.sqlite_pool {
|
||||
sqlx::query_as::<Sqlite, Student>(
|
||||
"SELECT id, name, score, reward_points, tags, extra_json, created_at, updated_at FROM students WHERE name = ?"
|
||||
"SELECT id, name, group_name, score, reward_points, tags, extra_json, created_at, updated_at FROM students WHERE name = ?"
|
||||
)
|
||||
.bind(name)
|
||||
.fetch_optional(pool)
|
||||
.await
|
||||
} else if let Some(pool) = &self.postgres_pool {
|
||||
sqlx::query_as::<Postgres, Student>(
|
||||
"SELECT id, name, score, reward_points, tags, extra_json, created_at, updated_at FROM students WHERE name = $1"
|
||||
"SELECT id, name, group_name, score, reward_points, tags, extra_json, created_at, updated_at FROM students WHERE name = $1"
|
||||
)
|
||||
.bind(name)
|
||||
.fetch_optional(pool)
|
||||
|
||||
@@ -13,6 +13,7 @@ pub mod students {
|
||||
pub const TABLE: &str = "students";
|
||||
pub const ID: &str = "id";
|
||||
pub const NAME: &str = "name";
|
||||
pub const GROUP_NAME: &str = "group_name";
|
||||
pub const TAGS: &str = "tags";
|
||||
pub const SCORE: &str = "score";
|
||||
pub const REWARD_POINTS: &str = "reward_points";
|
||||
@@ -107,6 +108,7 @@ pub fn get_create_students_table_sql(sqlite: bool) -> String {
|
||||
CREATE TABLE IF NOT EXISTS students (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
group_name TEXT,
|
||||
tags TEXT DEFAULT '[]',
|
||||
score INTEGER DEFAULT 0,
|
||||
reward_points INTEGER DEFAULT 0,
|
||||
@@ -121,6 +123,7 @@ pub fn get_create_students_table_sql(sqlite: bool) -> String {
|
||||
CREATE TABLE IF NOT EXISTS students (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
group_name TEXT,
|
||||
tags TEXT DEFAULT '[]',
|
||||
score INTEGER DEFAULT 0,
|
||||
reward_points INTEGER DEFAULT 0,
|
||||
|
||||
@@ -4,6 +4,7 @@ use serde::{Deserialize, Serialize};
|
||||
pub struct Student {
|
||||
pub id: i32,
|
||||
pub name: String,
|
||||
pub group_name: Option<String>,
|
||||
pub score: i32,
|
||||
pub reward_points: i32,
|
||||
pub tags: String,
|
||||
@@ -15,6 +16,7 @@ pub struct Student {
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct StudentUpdate {
|
||||
pub name: Option<String>,
|
||||
pub group_name: Option<String>,
|
||||
pub score: Option<i32>,
|
||||
pub reward_points: Option<i32>,
|
||||
pub tags: Option<Vec<String>>,
|
||||
@@ -25,6 +27,7 @@ pub struct StudentUpdate {
|
||||
pub struct StudentWithTags {
|
||||
pub id: i32,
|
||||
pub name: String,
|
||||
pub group_name: Option<String>,
|
||||
pub score: i32,
|
||||
pub reward_points: i32,
|
||||
pub tags: Vec<String>,
|
||||
@@ -37,6 +40,7 @@ impl From<Student> for StudentWithTags {
|
||||
Self {
|
||||
id: student.id,
|
||||
name: student.name,
|
||||
group_name: student.group_name,
|
||||
score: student.score,
|
||||
reward_points: student.reward_points,
|
||||
tags,
|
||||
|
||||
Reference in New Issue
Block a user