feat: initialize Nuanji (Warm Notes) project
- Base platform from base.git (ERP base: auth, core, config, message, workflow, plugin) - Created erp-diary module skeleton (lib.rs, dto.rs, error.rs, event.rs, state.rs) - Integrated erp-diary into workspace and erp-server - Added DiaryModule registration in main.rs - Added DiaryState FromRef in state.rs - Diary routes mounted (empty routes, ready for implementation) - Product design spec v1.2 preserved in docs/ - Implementation plan preserved in plans/ Cargo check: OK Cargo test: OK (78+ base tests passing)
This commit is contained in:
125
crates/erp-diary/src/dto.rs
Normal file
125
crates/erp-diary/src/dto.rs
Normal file
@@ -0,0 +1,125 @@
|
||||
// erp-diary 数据传输对象 (DTO)
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
/// 日记心情枚举
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum Mood {
|
||||
Happy,
|
||||
Calm,
|
||||
Sad,
|
||||
Angry,
|
||||
Thinking,
|
||||
}
|
||||
|
||||
/// 天气枚举
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum Weather {
|
||||
Sunny,
|
||||
Cloudy,
|
||||
Rainy,
|
||||
Snowy,
|
||||
Windy,
|
||||
}
|
||||
|
||||
/// 创建日记请求
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
pub struct CreateJournalReq {
|
||||
pub title: String,
|
||||
pub date: chrono::NaiveDate,
|
||||
pub mood: Mood,
|
||||
pub weather: Weather,
|
||||
pub tags: Vec<String>,
|
||||
pub is_private: bool,
|
||||
pub class_id: Option<uuid::Uuid>,
|
||||
pub assigned_topic_id: Option<uuid::Uuid>,
|
||||
}
|
||||
|
||||
/// 更新日记请求
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
pub struct UpdateJournalReq {
|
||||
pub title: Option<String>,
|
||||
pub mood: Option<Mood>,
|
||||
pub weather: Option<Weather>,
|
||||
pub tags: Option<Vec<String>>,
|
||||
pub is_private: Option<bool>,
|
||||
pub shared_to_class: Option<bool>,
|
||||
pub version: i32,
|
||||
}
|
||||
|
||||
/// 日记响应
|
||||
#[derive(Debug, Serialize, ToSchema)]
|
||||
pub struct JournalResp {
|
||||
pub id: uuid::Uuid,
|
||||
pub author_id: uuid::Uuid,
|
||||
pub class_id: Option<uuid::Uuid>,
|
||||
pub title: String,
|
||||
pub date: chrono::NaiveDate,
|
||||
pub mood: Mood,
|
||||
pub weather: Weather,
|
||||
pub tags: Vec<String>,
|
||||
pub is_private: bool,
|
||||
pub shared_to_class: bool,
|
||||
pub version: i32,
|
||||
pub created_at: chrono::DateTime<chrono::Utc>,
|
||||
pub updated_at: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
/// 创建班级请求
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
pub struct CreateClassReq {
|
||||
pub name: String,
|
||||
pub school_name: Option<String>,
|
||||
}
|
||||
|
||||
/// 加入班级请求
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
pub struct JoinClassReq {
|
||||
pub class_code: String,
|
||||
}
|
||||
|
||||
/// 班级响应
|
||||
#[derive(Debug, Serialize, ToSchema)]
|
||||
pub struct ClassResp {
|
||||
pub id: uuid::Uuid,
|
||||
pub name: String,
|
||||
pub school_name: Option<String>,
|
||||
pub teacher_id: uuid::Uuid,
|
||||
pub class_code: String,
|
||||
pub member_count: i32,
|
||||
pub is_active: bool,
|
||||
}
|
||||
|
||||
/// 同步请求
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
pub struct SyncReq {
|
||||
pub last_sync_time: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub changes: Vec<SyncChange>,
|
||||
}
|
||||
|
||||
/// 同步变更条目
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
pub enum SyncChange {
|
||||
CreateJournal { data: serde_json::Value },
|
||||
UpdateJournal { id: uuid::Uuid, version: i32, data: serde_json::Value },
|
||||
DeleteJournal { id: uuid::Uuid, version: i32 },
|
||||
}
|
||||
|
||||
/// 同步响应
|
||||
#[derive(Debug, Serialize, ToSchema)]
|
||||
pub struct SyncResp {
|
||||
pub server_changes: Vec<serde_json::Value>,
|
||||
pub conflicts: Vec<ConflictInfo>,
|
||||
pub sync_time: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
/// 冲突信息
|
||||
#[derive(Debug, Serialize, ToSchema)]
|
||||
pub struct ConflictInfo {
|
||||
pub journal_id: uuid::Uuid,
|
||||
pub local_version: i32,
|
||||
pub server_version: i32,
|
||||
}
|
||||
2
crates/erp-diary/src/entity/mod.rs
Normal file
2
crates/erp-diary/src/entity/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
// erp-diary SeaORM 实体占位
|
||||
// 后续 Phase B1 会定义完整的 ~15 个实体
|
||||
75
crates/erp-diary/src/error.rs
Normal file
75
crates/erp-diary/src/error.rs
Normal file
@@ -0,0 +1,75 @@
|
||||
// erp-diary 错误类型
|
||||
|
||||
use axum::http::StatusCode;
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum DiaryError {
|
||||
#[error("日记未找到: {0}")]
|
||||
NotFound(String),
|
||||
|
||||
#[error("版本冲突: 本地版本 {local}, 服务端版本 {server}")]
|
||||
VersionConflict { local: i32, server: i32 },
|
||||
|
||||
#[error("班级码无效")]
|
||||
InvalidClassCode,
|
||||
|
||||
#[error("班级码已过期")]
|
||||
ClassCodeExpired,
|
||||
|
||||
#[error("班级码尝试次数过多,请 {lockout_minutes} 分钟后重试")]
|
||||
ClassCodeLocked { lockout_minutes: u32 },
|
||||
|
||||
#[error("无权限执行此操作")]
|
||||
Forbidden,
|
||||
|
||||
#[error("内容安全检查未通过")]
|
||||
ContentSafetyViolation,
|
||||
|
||||
#[error("同步失败: {0}")]
|
||||
SyncFailed(String),
|
||||
|
||||
#[error("{0}")]
|
||||
BadRequest(String),
|
||||
|
||||
#[error("内部错误: {0}")]
|
||||
Internal(String),
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ErrorBody {
|
||||
error: String,
|
||||
message: String,
|
||||
}
|
||||
|
||||
impl IntoResponse for DiaryError {
|
||||
fn into_response(self) -> Response {
|
||||
let (status, message) = match &self {
|
||||
DiaryError::NotFound(_) => (StatusCode::NOT_FOUND, self.to_string()),
|
||||
DiaryError::VersionConflict { .. } => (StatusCode::CONFLICT, self.to_string()),
|
||||
DiaryError::InvalidClassCode | DiaryError::ClassCodeExpired => {
|
||||
(StatusCode::BAD_REQUEST, self.to_string())
|
||||
}
|
||||
DiaryError::ClassCodeLocked { .. } => (StatusCode::TOO_MANY_REQUESTS, self.to_string()),
|
||||
DiaryError::Forbidden => (StatusCode::FORBIDDEN, self.to_string()),
|
||||
DiaryError::ContentSafetyViolation => (StatusCode::BAD_REQUEST, self.to_string()),
|
||||
DiaryError::SyncFailed(_) => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()),
|
||||
DiaryError::BadRequest(_) => (StatusCode::BAD_REQUEST, self.to_string()),
|
||||
DiaryError::Internal(_) => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()),
|
||||
};
|
||||
|
||||
let body = ErrorBody {
|
||||
error: format!("diary.{}", status.as_u16()),
|
||||
message,
|
||||
};
|
||||
|
||||
(status, axum::Json(body)).into_response()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<sea_orm::DbErr> for DiaryError {
|
||||
fn from(err: sea_orm::DbErr) -> Self {
|
||||
DiaryError::Internal(err.to_string())
|
||||
}
|
||||
}
|
||||
61
crates/erp-diary/src/event.rs
Normal file
61
crates/erp-diary/src/event.rs
Normal file
@@ -0,0 +1,61 @@
|
||||
// erp-diary 事件定义
|
||||
|
||||
/// 日记模块领域事件
|
||||
pub enum DiaryEvent {
|
||||
/// 日记创建
|
||||
JournalCreated {
|
||||
journal_id: uuid::Uuid,
|
||||
author_id: uuid::Uuid,
|
||||
class_id: Option<uuid::Uuid>,
|
||||
},
|
||||
/// 日记更新
|
||||
JournalUpdated {
|
||||
journal_id: uuid::Uuid,
|
||||
author_id: uuid::Uuid,
|
||||
version: i32,
|
||||
},
|
||||
/// 日记删除
|
||||
JournalDeleted {
|
||||
journal_id: uuid::Uuid,
|
||||
author_id: uuid::Uuid,
|
||||
},
|
||||
/// 日记分享到班级
|
||||
JournalShared {
|
||||
journal_id: uuid::Uuid,
|
||||
author_id: uuid::Uuid,
|
||||
class_id: uuid::Uuid,
|
||||
},
|
||||
/// 班级创建
|
||||
ClassCreated {
|
||||
class_id: uuid::Uuid,
|
||||
teacher_id: uuid::Uuid,
|
||||
},
|
||||
/// 学生加入班级
|
||||
StudentJoinedClass {
|
||||
class_id: uuid::Uuid,
|
||||
student_id: uuid::Uuid,
|
||||
},
|
||||
/// 老师布置主题
|
||||
TopicAssigned {
|
||||
topic_id: uuid::Uuid,
|
||||
class_id: uuid::Uuid,
|
||||
teacher_id: uuid::Uuid,
|
||||
},
|
||||
/// 老师点评
|
||||
CommentCreated {
|
||||
comment_id: uuid::Uuid,
|
||||
journal_id: uuid::Uuid,
|
||||
teacher_id: uuid::Uuid,
|
||||
student_id: uuid::Uuid,
|
||||
},
|
||||
/// 家长绑定孩子
|
||||
ParentBound {
|
||||
parent_id: uuid::Uuid,
|
||||
child_id: uuid::Uuid,
|
||||
},
|
||||
/// 成就解锁
|
||||
AchievementUnlocked {
|
||||
user_id: uuid::Uuid,
|
||||
achievement_id: String,
|
||||
},
|
||||
}
|
||||
2
crates/erp-diary/src/handler/mod.rs
Normal file
2
crates/erp-diary/src/handler/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
// erp-diary API 处理器占位
|
||||
// 后续 Phase B2-B7 会实现 ~10 个处理器
|
||||
112
crates/erp-diary/src/lib.rs
Normal file
112
crates/erp-diary/src/lib.rs
Normal file
@@ -0,0 +1,112 @@
|
||||
pub mod entity;
|
||||
pub mod service;
|
||||
pub mod handler;
|
||||
pub mod dto;
|
||||
pub mod error;
|
||||
pub mod event;
|
||||
pub mod state;
|
||||
|
||||
pub use state::DiaryState;
|
||||
|
||||
use erp_core::module::ErpModule;
|
||||
|
||||
/// 暖记日记业务模块
|
||||
pub struct DiaryModule;
|
||||
|
||||
impl ErpModule for DiaryModule {
|
||||
fn name(&self) -> &str {
|
||||
"diary"
|
||||
}
|
||||
|
||||
fn id(&self) -> &str {
|
||||
"erp-diary"
|
||||
}
|
||||
|
||||
fn version(&self) -> &str {
|
||||
env!("CARGO_PKG_VERSION")
|
||||
}
|
||||
|
||||
fn module_type(&self) -> erp_core::module::ModuleType {
|
||||
erp_core::module::ModuleType::Builtin
|
||||
}
|
||||
|
||||
fn dependencies(&self) -> Vec<&str> {
|
||||
vec!["erp-auth", "erp-core"]
|
||||
}
|
||||
|
||||
fn permissions(&self) -> Vec<erp_core::module::PermissionDescriptor> {
|
||||
vec![
|
||||
erp_core::module::PermissionDescriptor {
|
||||
code: "diary.journal.create".into(),
|
||||
name: "创建日记".into(),
|
||||
module: "diary".into(),
|
||||
description: "允许创建日记条目".into(),
|
||||
},
|
||||
erp_core::module::PermissionDescriptor {
|
||||
code: "diary.journal.read".into(),
|
||||
name: "查看日记".into(),
|
||||
module: "diary".into(),
|
||||
description: "允许查看日记条目".into(),
|
||||
},
|
||||
erp_core::module::PermissionDescriptor {
|
||||
code: "diary.journal.update".into(),
|
||||
name: "编辑日记".into(),
|
||||
module: "diary".into(),
|
||||
description: "允许编辑日记条目".into(),
|
||||
},
|
||||
erp_core::module::PermissionDescriptor {
|
||||
code: "diary.journal.delete".into(),
|
||||
name: "删除日记".into(),
|
||||
module: "diary".into(),
|
||||
description: "允许删除日记条目".into(),
|
||||
},
|
||||
erp_core::module::PermissionDescriptor {
|
||||
code: "diary.class.manage".into(),
|
||||
name: "管理班级".into(),
|
||||
module: "diary".into(),
|
||||
description: "允许创建和管理班级".into(),
|
||||
},
|
||||
erp_core::module::PermissionDescriptor {
|
||||
code: "diary.topic.assign".into(),
|
||||
name: "布置主题".into(),
|
||||
module: "diary".into(),
|
||||
description: "允许老师布置日记主题".into(),
|
||||
},
|
||||
erp_core::module::PermissionDescriptor {
|
||||
code: "diary.comment.write".into(),
|
||||
name: "写评语".into(),
|
||||
module: "diary".into(),
|
||||
description: "允许老师点评日记".into(),
|
||||
},
|
||||
erp_core::module::PermissionDescriptor {
|
||||
code: "diary.parent.bind".into(),
|
||||
name: "家长绑定".into(),
|
||||
module: "diary".into(),
|
||||
description: "允许家长绑定孩子账号".into(),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl DiaryModule {
|
||||
/// 公开路由(无需认证)
|
||||
pub fn public_routes<S>() -> axum::Router<S>
|
||||
where
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
axum::Router::new()
|
||||
}
|
||||
|
||||
/// 受保护路由(需要 JWT 认证)
|
||||
pub fn protected_routes<S>() -> axum::Router<S>
|
||||
where
|
||||
crate::state::DiaryState: axum::extract::FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
axum::Router::new()
|
||||
}
|
||||
}
|
||||
2
crates/erp-diary/src/service/mod.rs
Normal file
2
crates/erp-diary/src/service/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
// erp-diary 业务服务占位
|
||||
// 后续 Phase B2-B6 会实现 ~12 个服务
|
||||
13
crates/erp-diary/src/state.rs
Normal file
13
crates/erp-diary/src/state.rs
Normal file
@@ -0,0 +1,13 @@
|
||||
// erp-diary State 定义
|
||||
|
||||
use sea_orm::DatabaseConnection;
|
||||
use erp_core::crypto::PiiCrypto;
|
||||
use erp_core::events::EventBus;
|
||||
|
||||
/// 暖记模块状态,通过 Axum State 提取
|
||||
#[derive(Clone)]
|
||||
pub struct DiaryState {
|
||||
pub db: DatabaseConnection,
|
||||
pub event_bus: EventBus,
|
||||
pub crypto: PiiCrypto,
|
||||
}
|
||||
Reference in New Issue
Block a user