docs(ai): 实施计划 Chunk 2 (数据库迁移 + SeaORM Entity)
This commit is contained in:
@@ -402,3 +402,386 @@ git commit -m "feat(health): 添加 HealthDataProvider stub 实现"
|
|||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Chunk 2: 数据库迁移 + SeaORM Entity
|
||||||
|
|
||||||
|
### Task 4: 创建 ai_prompts / ai_analysis_results / ai_usage_logs 迁移
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `crates/erp-server/migration/src/m20260425_000050_create_ai_tables.rs`
|
||||||
|
- Modify: `crates/erp-server/migration/src/lib.rs` — 注册到 Migrator
|
||||||
|
|
||||||
|
> 先查看 `migration/src/lib.rs` 最后一个 migration 编号,确保 `000050` 不冲突。
|
||||||
|
|
||||||
|
- [ ] **Step 1: 创建迁移文件**
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// crates/erp-server/migration/src/m20260425_000050_create_ai_tables.rs
|
||||||
|
use sea_orm_migration::prelude::*;
|
||||||
|
|
||||||
|
#[derive(DeriveMigrationName)]
|
||||||
|
pub struct Migration;
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl MigrationTrait for Migration {
|
||||||
|
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||||
|
// 1. ai_prompts — Prompt 模板存储
|
||||||
|
manager.create_table(
|
||||||
|
Table::create()
|
||||||
|
.table(AiPrompt::Table)
|
||||||
|
.if_not_exists()
|
||||||
|
.col(ColumnDef::new(AiPrompt::Id).uuid().not_null().primary_key())
|
||||||
|
.col(ColumnDef::new(AiPrompt::TenantId).uuid().not_null())
|
||||||
|
.col(ColumnDef::new(AiPrompt::Name).string_len(100).not_null())
|
||||||
|
.col(ColumnDef::new(AiPrompt::Description).text().not_null().default(""))
|
||||||
|
.col(ColumnDef::new(AiPrompt::SystemPrompt).text().not_null())
|
||||||
|
.col(ColumnDef::new(AiPrompt::UserPromptTemplate).text().not_null())
|
||||||
|
.col(ColumnDef::new(AiPrompt::VariablesSchema).json().null())
|
||||||
|
.col(ColumnDef::new(AiPrompt::ModelConfig).json().not_null())
|
||||||
|
.col(ColumnDef::new(AiPrompt::Version).integer().not_null().default(1))
|
||||||
|
.col(ColumnDef::new(AiPrompt::IsActive).boolean().not_null().default(true))
|
||||||
|
.col(ColumnDef::new(AiPrompt::Category).string_len(50).not_null().default("analysis"))
|
||||||
|
.col(ColumnDef::new(AiPrompt::Tags).json().null())
|
||||||
|
.col(ColumnDef::new(AiPrompt::CreatedAt).timestamp_with_time_zone().not_null().default(Expr::current_timestamp()))
|
||||||
|
.col(ColumnDef::new(AiPrompt::UpdatedAt).timestamp_with_time_zone().not_null().default(Expr::current_timestamp()))
|
||||||
|
.col(ColumnDef::new(AiPrompt::CreatedBy).uuid().null())
|
||||||
|
.col(ColumnDef::new(AiPrompt::UpdatedBy).uuid().null())
|
||||||
|
.col(ColumnDef::new(AiPrompt::DeletedAt).timestamp_with_time_zone().null())
|
||||||
|
.col(ColumnDef::new(AiPrompt::VersionLock).integer().not_null().default(1))
|
||||||
|
.to_owned(),
|
||||||
|
).await?;
|
||||||
|
|
||||||
|
// ai_prompts 索引
|
||||||
|
manager.create_index(
|
||||||
|
Index::create()
|
||||||
|
.if_not_exists()
|
||||||
|
.name("idx_ai_prompts_tenant_name")
|
||||||
|
.table(AiPrompt::Table)
|
||||||
|
.col(AiPrompt::TenantId)
|
||||||
|
.col(AiPrompt::Name)
|
||||||
|
.col(AiPrompt::IsActive)
|
||||||
|
.to_owned(),
|
||||||
|
).await?;
|
||||||
|
|
||||||
|
// 2. ai_analysis_results — 分析结果存储
|
||||||
|
manager.create_table(
|
||||||
|
Table::create()
|
||||||
|
.table(AiAnalysis::Table)
|
||||||
|
.if_not_exists()
|
||||||
|
.col(ColumnDef::new(AiAnalysis::Id).uuid().not_null().primary_key())
|
||||||
|
.col(ColumnDef::new(AiAnalysis::TenantId).uuid().not_null())
|
||||||
|
.col(ColumnDef::new(AiAnalysis::PatientId).uuid().not_null())
|
||||||
|
.col(ColumnDef::new(AiAnalysis::AnalysisType).string_len(50).not_null())
|
||||||
|
.col(ColumnDef::new(AiAnalysis::SourceRef).string_len(200).not_null())
|
||||||
|
.col(ColumnDef::new(AiAnalysis::PromptId).uuid().not_null())
|
||||||
|
.col(ColumnDef::new(AiAnalysis::PromptVersion).integer().not_null())
|
||||||
|
.col(ColumnDef::new(AiAnalysis::ModelUsed).string_len(100).not_null())
|
||||||
|
.col(ColumnDef::new(AiAnalysis::InputDataHash).string_len(64).not_null())
|
||||||
|
.col(ColumnDef::new(AiAnalysis::SanitizedInput).json().null())
|
||||||
|
.col(ColumnDef::new(AiAnalysis::ResultContent).text().null())
|
||||||
|
.col(ColumnDef::new(AiAnalysis::ResultMetadata).json().null())
|
||||||
|
.col(ColumnDef::new(AiAnalysis::Status).string_len(20).not_null().default("pending"))
|
||||||
|
.col(ColumnDef::new(AiAnalysis::ErrorMessage).text().null())
|
||||||
|
.col(ColumnDef::new(AiAnalysis::CreatedAt).timestamp_with_time_zone().not_null().default(Expr::current_timestamp()))
|
||||||
|
.col(ColumnDef::new(AiAnalysis::UpdatedAt).timestamp_with_time_zone().not_null().default(Expr::current_timestamp()))
|
||||||
|
.col(ColumnDef::new(AiAnalysis::CreatedBy).uuid().null())
|
||||||
|
.col(ColumnDef::new(AiAnalysis::UpdatedBy).uuid().null())
|
||||||
|
.col(ColumnDef::new(AiAnalysis::DeletedAt).timestamp_with_time_zone().null())
|
||||||
|
.col(ColumnDef::new(AiAnalysis::VersionLock).integer().not_null().default(1))
|
||||||
|
.to_owned(),
|
||||||
|
).await?;
|
||||||
|
|
||||||
|
// ai_analysis 索引
|
||||||
|
manager.create_index(
|
||||||
|
Index::create()
|
||||||
|
.if_not_exists()
|
||||||
|
.name("idx_ai_analysis_tenant_patient")
|
||||||
|
.table(AiAnalysis::Table)
|
||||||
|
.col(AiAnalysis::TenantId)
|
||||||
|
.col(AiAnalysis::PatientId)
|
||||||
|
.to_owned(),
|
||||||
|
).await?;
|
||||||
|
|
||||||
|
manager.create_index(
|
||||||
|
Index::create()
|
||||||
|
.if_not_exists()
|
||||||
|
.name("idx_ai_analysis_cache_hash")
|
||||||
|
.table(AiAnalysis::Table)
|
||||||
|
.col(AiAnalysis::InputDataHash)
|
||||||
|
.to_owned(),
|
||||||
|
).await?;
|
||||||
|
|
||||||
|
// 3. ai_usage_logs — AI 调用计量
|
||||||
|
manager.create_table(
|
||||||
|
Table::create()
|
||||||
|
.table(AiUsage::Table)
|
||||||
|
.if_not_exists()
|
||||||
|
.col(ColumnDef::new(AiUsage::Id).uuid().not_null().primary_key())
|
||||||
|
.col(ColumnDef::new(AiUsage::TenantId).uuid().not_null())
|
||||||
|
.col(ColumnDef::new(AiUsage::Provider).string_len(50).not_null())
|
||||||
|
.col(ColumnDef::new(AiUsage::Model).string_len(100).not_null())
|
||||||
|
.col(ColumnDef::new(AiUsage::AnalysisType).string_len(50).not_null())
|
||||||
|
.col(ColumnDef::new(AiUsage::InputTokens).integer().not_null().default(0))
|
||||||
|
.col(ColumnDef::new(AiUsage::OutputTokens).integer().not_null().default(0))
|
||||||
|
.col(ColumnDef::new(AiUsage::DurationMs).integer().not_null().default(0))
|
||||||
|
.col(ColumnDef::new(AiUsage::CostCents).integer().not_null().default(0))
|
||||||
|
.col(ColumnDef::new(AiUsage::IsCacheHit).boolean().not_null().default(false))
|
||||||
|
.col(ColumnDef::new(AiUsage::CreatedAt).timestamp_with_time_zone().not_null().default(Expr::current_timestamp()))
|
||||||
|
.to_owned(),
|
||||||
|
).await?;
|
||||||
|
|
||||||
|
// ai_usage 索引
|
||||||
|
manager.create_index(
|
||||||
|
Index::create()
|
||||||
|
.if_not_exists()
|
||||||
|
.name("idx_ai_usage_tenant_created")
|
||||||
|
.table(AiUsage::Table)
|
||||||
|
.col(AiUsage::TenantId)
|
||||||
|
.col(AiUsage::CreatedAt)
|
||||||
|
.to_owned(),
|
||||||
|
).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||||
|
manager.drop_table(Table::drop().table(AiUsage::Table).to_owned()).await?;
|
||||||
|
manager.drop_table(Table::drop().table(AiAnalysis::Table).to_owned()).await?;
|
||||||
|
manager.drop_table(Table::drop().table(AiPrompt::Table).to_owned()).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(DeriveIden)]
|
||||||
|
enum AiPrompt {
|
||||||
|
Table,
|
||||||
|
Id,
|
||||||
|
TenantId,
|
||||||
|
Name,
|
||||||
|
Description,
|
||||||
|
SystemPrompt,
|
||||||
|
UserPromptTemplate,
|
||||||
|
VariablesSchema,
|
||||||
|
ModelConfig,
|
||||||
|
Version,
|
||||||
|
IsActive,
|
||||||
|
Category,
|
||||||
|
Tags,
|
||||||
|
CreatedAt,
|
||||||
|
UpdatedAt,
|
||||||
|
CreatedBy,
|
||||||
|
UpdatedBy,
|
||||||
|
DeletedAt,
|
||||||
|
VersionLock,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(DeriveIden)]
|
||||||
|
enum AiAnalysis {
|
||||||
|
Table,
|
||||||
|
Id,
|
||||||
|
TenantId,
|
||||||
|
PatientId,
|
||||||
|
AnalysisType,
|
||||||
|
SourceRef,
|
||||||
|
PromptId,
|
||||||
|
PromptVersion,
|
||||||
|
ModelUsed,
|
||||||
|
InputDataHash,
|
||||||
|
SanitizedInput,
|
||||||
|
ResultContent,
|
||||||
|
ResultMetadata,
|
||||||
|
Status,
|
||||||
|
ErrorMessage,
|
||||||
|
CreatedAt,
|
||||||
|
UpdatedAt,
|
||||||
|
CreatedBy,
|
||||||
|
UpdatedBy,
|
||||||
|
DeletedAt,
|
||||||
|
VersionLock,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(DeriveIden)]
|
||||||
|
enum AiUsage {
|
||||||
|
Table,
|
||||||
|
Id,
|
||||||
|
TenantId,
|
||||||
|
Provider,
|
||||||
|
Model,
|
||||||
|
AnalysisType,
|
||||||
|
InputTokens,
|
||||||
|
OutputTokens,
|
||||||
|
DurationMs,
|
||||||
|
CostCents,
|
||||||
|
IsCacheHit,
|
||||||
|
CreatedAt,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 在 migration/src/lib.rs 注册**
|
||||||
|
|
||||||
|
在 `Migrator::migrations()` 的 `vec![]` 末尾添加:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
Box::new(m20260425_000050_create_ai_tables::Migration),
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: 验证迁移编译**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo check -p erp-server
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: 提交**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add crates/erp-server/migration/src/
|
||||||
|
git commit -m "feat(db): 添加 ai_prompts / ai_analysis_results / ai_usage_logs 迁移"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 5: 创建 SeaORM Entity
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `crates/erp-ai/src/entity/mod.rs`
|
||||||
|
- Create: `crates/erp-ai/src/entity/ai_prompt.rs`
|
||||||
|
- Create: `crates/erp-ai/src/entity/ai_analysis.rs`
|
||||||
|
- Create: `crates/erp-ai/src/entity/ai_usage.rs`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 创建 entity/mod.rs**
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// crates/erp-ai/src/entity/mod.rs
|
||||||
|
pub mod ai_analysis;
|
||||||
|
pub mod ai_prompt;
|
||||||
|
pub mod ai_usage;
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 创建 ai_prompt.rs**
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// crates/erp-ai/src/entity/ai_prompt.rs
|
||||||
|
use sea_orm::entity::prelude::*;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
|
||||||
|
#[sea_orm(table_name = "ai_prompts")]
|
||||||
|
pub struct Model {
|
||||||
|
#[sea_orm(primary_key, auto_increment = false)]
|
||||||
|
pub id: Uuid,
|
||||||
|
pub tenant_id: Uuid,
|
||||||
|
pub name: String,
|
||||||
|
pub description: String,
|
||||||
|
pub system_prompt: String,
|
||||||
|
pub user_prompt_template: String,
|
||||||
|
pub variables_schema: Option<serde_json::Value>,
|
||||||
|
pub model_config: serde_json::Value,
|
||||||
|
pub version: i32,
|
||||||
|
pub is_active: bool,
|
||||||
|
pub category: String,
|
||||||
|
pub tags: Option<serde_json::Value>,
|
||||||
|
pub created_at: DateTimeUtc,
|
||||||
|
pub updated_at: DateTimeUtc,
|
||||||
|
pub created_by: Option<Uuid>,
|
||||||
|
pub updated_by: Option<Uuid>,
|
||||||
|
pub deleted_at: Option<DateTimeUtc>,
|
||||||
|
pub version_lock: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||||
|
pub enum Relation {}
|
||||||
|
|
||||||
|
impl ActiveModelBehavior for ActiveModel {}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: 创建 ai_analysis.rs**
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// crates/erp-ai/src/entity/ai_analysis.rs
|
||||||
|
use sea_orm::entity::prelude::*;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
|
||||||
|
#[sea_orm(table_name = "ai_analysis_results")]
|
||||||
|
pub struct Model {
|
||||||
|
#[sea_orm(primary_key, auto_increment = false)]
|
||||||
|
pub id: Uuid,
|
||||||
|
pub tenant_id: Uuid,
|
||||||
|
pub patient_id: Uuid,
|
||||||
|
pub analysis_type: String,
|
||||||
|
pub source_ref: String,
|
||||||
|
pub prompt_id: Uuid,
|
||||||
|
pub prompt_version: i32,
|
||||||
|
pub model_used: String,
|
||||||
|
pub input_data_hash: String,
|
||||||
|
pub sanitized_input: Option<serde_json::Value>,
|
||||||
|
pub result_content: Option<String>,
|
||||||
|
pub result_metadata: Option<serde_json::Value>,
|
||||||
|
pub status: String,
|
||||||
|
pub error_message: Option<String>,
|
||||||
|
pub created_at: DateTimeUtc,
|
||||||
|
pub updated_at: DateTimeUtc,
|
||||||
|
pub created_by: Option<Uuid>,
|
||||||
|
pub updated_by: Option<Uuid>,
|
||||||
|
pub deleted_at: Option<DateTimeUtc>,
|
||||||
|
pub version_lock: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||||
|
pub enum Relation {}
|
||||||
|
|
||||||
|
impl ActiveModelBehavior for ActiveModel {}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: 创建 ai_usage.rs**
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// crates/erp-ai/src/entity/ai_usage.rs
|
||||||
|
use sea_orm::entity::prelude::*;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
|
||||||
|
#[sea_orm(table_name = "ai_usage_logs")]
|
||||||
|
pub struct Model {
|
||||||
|
#[sea_orm(primary_key, auto_increment = false)]
|
||||||
|
pub id: Uuid,
|
||||||
|
pub tenant_id: Uuid,
|
||||||
|
pub provider: String,
|
||||||
|
pub model: String,
|
||||||
|
pub analysis_type: String,
|
||||||
|
pub input_tokens: i32,
|
||||||
|
pub output_tokens: i32,
|
||||||
|
pub duration_ms: i32,
|
||||||
|
pub cost_cents: i32,
|
||||||
|
pub is_cache_hit: bool,
|
||||||
|
pub created_at: DateTimeUtc,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||||
|
pub enum Relation {}
|
||||||
|
|
||||||
|
impl ActiveModelBehavior for ActiveModel {}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: 更新 lib.rs 添加 entity mod**
|
||||||
|
|
||||||
|
在 `crates/erp-ai/src/lib.rs` 中添加:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub mod entity;
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 6: 验证编译**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo check -p erp-ai
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 7: 提交**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add crates/erp-ai/src/entity/ crates/erp-ai/src/lib.rs
|
||||||
|
git commit -m "feat(ai): 添加 SeaORM Entity (ai_prompt/ai_analysis/ai_usage)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|||||||
Reference in New Issue
Block a user