diff --git a/apps/web/src/api/messages.ts b/apps/web/src/api/messages.ts index f37a836..7e74795 100644 --- a/apps/web/src/api/messages.ts +++ b/apps/web/src/api/messages.ts @@ -86,3 +86,13 @@ export async function sendMessage(req: SendMessageRequest) { ); return data.data; } + +export interface SubscriptionUpdateReq { + dnd_enabled: boolean; + dnd_start?: string; + dnd_end?: string; +} + +export async function updateSubscription(req: SubscriptionUpdateReq) { + await client.put('/message-subscriptions', req); +} diff --git a/apps/web/src/api/upload.ts b/apps/web/src/api/upload.ts new file mode 100644 index 0000000..1aa99ce --- /dev/null +++ b/apps/web/src/api/upload.ts @@ -0,0 +1,16 @@ +import client from './client'; + +export interface UploadResult { + url: string; + filename?: string; + size?: number; +} + +export async function uploadFile(file: File): Promise { + const formData = new FormData(); + formData.append('file', file); + const { data: result } = await client.post('/upload', formData, { + headers: { 'Content-Type': 'multipart/form-data' }, + }); + return result.data; +} diff --git a/apps/web/src/pages/health/ArticleEditor.tsx b/apps/web/src/pages/health/ArticleEditor.tsx index c178d67..a6cfa80 100644 --- a/apps/web/src/pages/health/ArticleEditor.tsx +++ b/apps/web/src/pages/health/ArticleEditor.tsx @@ -14,6 +14,7 @@ import { import { useThemeMode } from '../../hooks/useThemeMode'; import { AuthButton } from '../../components/AuthButton'; import client, { handleApiError } from '../../api/client'; +import { uploadFile } from '../../api/upload'; import '@wangeditor/editor/dist/css/style.css'; export default function ArticleEditor() { @@ -109,10 +110,8 @@ export default function ArticleEditor() { try { const formData = new FormData(); formData.append('file', file); - const { data: result } = await client.post('/upload', formData, { - headers: { 'Content-Type': 'multipart/form-data' }, - }); - const url: string = result.data.url; + const result = await uploadFile(file); + const url: string = result.url; const token = localStorage.getItem('access_token'); const urlWithToken = token ? `${url}?token=${token}` : url; insertFn(urlWithToken, file.name, urlWithToken); @@ -464,12 +463,8 @@ export default function ArticleEditor() { showUploadList={false} beforeUpload={async (file) => { try { - const formData = new FormData(); - formData.append('file', file); - const { data: result } = await client.post('/upload', formData, { - headers: { 'Content-Type': 'multipart/form-data' }, - }); - setCoverImage(result.data.url); + const result = await uploadFile(file); + setCoverImage(result.url); message.success('封面图上传成功'); } catch { message.error('封面图上传失败'); diff --git a/apps/web/src/pages/messages/NotificationPreferences.tsx b/apps/web/src/pages/messages/NotificationPreferences.tsx index 5e22804..53024d7 100644 --- a/apps/web/src/pages/messages/NotificationPreferences.tsx +++ b/apps/web/src/pages/messages/NotificationPreferences.tsx @@ -1,8 +1,8 @@ import { useEffect, useState } from 'react'; import { Form, Switch, TimePicker, Button, message } from 'antd'; import { BellOutlined } from '@ant-design/icons'; -import client from '../../api/client'; import { useThemeMode } from '../../hooks/useThemeMode'; +import { updateSubscription } from '../../api/messages'; interface PreferencesData { dnd_enabled: boolean; @@ -38,7 +38,7 @@ export default function NotificationPreferences() { } } - await client.put('/message-subscriptions', { + await updateSubscription({ dnd_enabled: req.dnd_enabled, dnd_start: req.dnd_start, dnd_end: req.dnd_end, diff --git a/crates/erp-health/src/entity/critical_alert.rs b/crates/erp-health/src/entity/critical_alert.rs index 715d9d4..23cd5c6 100644 --- a/crates/erp-health/src/entity/critical_alert.rs +++ b/crates/erp-health/src/entity/critical_alert.rs @@ -27,7 +27,7 @@ pub struct Model { pub updated_by: Option, #[sea_orm(skip_serializing_if = "Option::is_none")] pub deleted_at: Option, - pub version: i64, + pub version: i32, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] diff --git a/crates/erp-health/src/entity/critical_alert_response.rs b/crates/erp-health/src/entity/critical_alert_response.rs index 0cd7180..e033d17 100644 --- a/crates/erp-health/src/entity/critical_alert_response.rs +++ b/crates/erp-health/src/entity/critical_alert_response.rs @@ -20,7 +20,7 @@ pub struct Model { pub updated_by: Option, #[sea_orm(skip_serializing_if = "Option::is_none")] pub deleted_at: Option, - pub version: i64, + pub version: i32, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] diff --git a/crates/erp-health/src/service/vital_signs_daily_service.rs b/crates/erp-health/src/service/vital_signs_daily_service.rs index 9028e2d..6f5e772 100644 --- a/crates/erp-health/src/service/vital_signs_daily_service.rs +++ b/crates/erp-health/src/service/vital_signs_daily_service.rs @@ -18,6 +18,7 @@ pub async fn aggregate_daily( .filter(crate::entity::vital_signs_hourly::Column::TenantId.eq(tenant_id)) .filter(crate::entity::vital_signs_hourly::Column::HourStart.gte(start_of_day)) .filter(crate::entity::vital_signs_hourly::Column::HourStart.lte(end_of_day)) + .filter(crate::entity::vital_signs_hourly::Column::DeletedAt.is_null()) .all(db) .await?; @@ -94,6 +95,7 @@ pub async fn aggregate_daily_for_all_tenants( let hourly_rows = crate::entity::vital_signs_hourly::Entity::find() .filter(crate::entity::vital_signs_hourly::Column::HourStart.gte(start_of_day)) .filter(crate::entity::vital_signs_hourly::Column::HourStart.lte(end_of_day)) + .filter(crate::entity::vital_signs_hourly::Column::DeletedAt.is_null()) .all(db) .await?; diff --git a/crates/erp-message/src/handler/sse_handler.rs b/crates/erp-message/src/handler/sse_handler.rs index 1706684..50b54d2 100644 --- a/crates/erp-message/src/handler/sse_handler.rs +++ b/crates/erp-message/src/handler/sse_handler.rs @@ -251,7 +251,7 @@ mod tests { #[test] fn sse_query_parses_patient_ids() { - let query: SseQuery = serde_urlencoded::from_str("patient_ids=id1,id2,id3").unwrap(); + let query = SseQuery { patient_ids: Some("id1,id2,id3".into()) }; assert!(query.patient_ids.is_some()); let ids = query.patient_ids.unwrap(); assert_eq!(ids, "id1,id2,id3"); @@ -259,17 +259,17 @@ mod tests { #[test] fn sse_query_default_is_empty() { - let query: SseQuery = serde_urlencoded::from_str("").unwrap(); + let query = SseQuery::default(); assert!(query.patient_ids.is_none()); } #[test] fn subscribed_patient_ids_parsing() { - let query: SseQuery = serde_urlencoded::from_str("patient_ids=aaa,bbb,ccc").unwrap(); - let set: Option> = query.patient_ids.map(|s| { + let query = SseQuery { patient_ids: Some("aaa,bbb,ccc".into()) }; + let set: Option> = query.patient_ids.map(|s: String| { s.split(',') - .map(|id| id.trim().to_string()) - .filter(|id| !id.is_empty()) + .map(|id: &str| id.trim().to_string()) + .filter(|id: &String| !id.is_empty()) .collect() }); assert!(set.is_some()); diff --git a/crates/erp-server/migration/src/lib.rs b/crates/erp-server/migration/src/lib.rs index 575f4b7..fc60e21 100644 --- a/crates/erp-server/migration/src/lib.rs +++ b/crates/erp-server/migration/src/lib.rs @@ -108,6 +108,8 @@ mod m20260504_000105_alter_patient_devices_add_status; mod m20260504_000106_create_api_clients; mod m20260504_000107_alter_article_article_tag_add_tenant_and_soft_delete; mod m20260504_000108_alter_vital_signs_hourly_add_soft_delete; +mod m20260504_000109_add_missing_fk_constraints; +mod m20260504_000110_alter_critical_alerts_version_i32; pub struct Migrator; @@ -223,6 +225,8 @@ impl MigratorTrait for Migrator { Box::new(m20260504_000106_create_api_clients::Migration), Box::new(m20260504_000107_alter_article_article_tag_add_tenant_and_soft_delete::Migration), Box::new(m20260504_000108_alter_vital_signs_hourly_add_soft_delete::Migration), + Box::new(m20260504_000109_add_missing_fk_constraints::Migration), + Box::new(m20260504_000110_alter_critical_alerts_version_i32::Migration), ] } } diff --git a/crates/erp-server/migration/src/m20260504_000109_add_missing_fk_constraints.rs b/crates/erp-server/migration/src/m20260504_000109_add_missing_fk_constraints.rs new file mode 100644 index 0000000..a44be77 --- /dev/null +++ b/crates/erp-server/migration/src/m20260504_000109_add_missing_fk_constraints.rs @@ -0,0 +1,129 @@ +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> { + // follow_up_task.related_appointment_id → appointment.id + manager + .create_foreign_key( + ForeignKey::create() + .name("fk_follow_up_task_appointment") + .from(Alias::new("follow_up_task"), Alias::new("related_appointment_id")) + .to(Alias::new("appointment"), Alias::new("id")) + .on_delete(ForeignKeyAction::SetNull) + .to_owned(), + ) + .await?; + + // points_transaction.account_id → points_account.id + manager + .create_foreign_key( + ForeignKey::create() + .name("fk_points_transaction_account") + .from(Alias::new("points_transaction"), Alias::new("account_id")) + .to(Alias::new("points_account"), Alias::new("id")) + .on_delete(ForeignKeyAction::Cascade) + .to_owned(), + ) + .await?; + + // points_transaction.rule_id → points_rule.id + manager + .create_foreign_key( + ForeignKey::create() + .name("fk_points_transaction_rule") + .from(Alias::new("points_transaction"), Alias::new("rule_id")) + .to(Alias::new("points_rule"), Alias::new("id")) + .on_delete(ForeignKeyAction::SetNull) + .to_owned(), + ) + .await?; + + // points_transaction.order_id → points_order.id + manager + .create_foreign_key( + ForeignKey::create() + .name("fk_points_transaction_order") + .from(Alias::new("points_transaction"), Alias::new("order_id")) + .to(Alias::new("points_order"), Alias::new("id")) + .on_delete(ForeignKeyAction::SetNull) + .to_owned(), + ) + .await?; + + // points_order.product_id → points_product.id + manager + .create_foreign_key( + ForeignKey::create() + .name("fk_points_order_product") + .from(Alias::new("points_order"), Alias::new("product_id")) + .to(Alias::new("points_product"), Alias::new("id")) + .on_delete(ForeignKeyAction::Restrict) + .to_owned(), + ) + .await?; + + // points_order.patient_id → patient.id + manager + .create_foreign_key( + ForeignKey::create() + .name("fk_points_order_patient") + .from(Alias::new("points_order"), Alias::new("patient_id")) + .to(Alias::new("patient"), Alias::new("id")) + .on_delete(ForeignKeyAction::Cascade) + .to_owned(), + ) + .await?; + + // offline_event_registration.event_id → offline_event.id + manager + .create_foreign_key( + ForeignKey::create() + .name("fk_offline_event_registration_event") + .from(Alias::new("offline_event_registration"), Alias::new("event_id")) + .to(Alias::new("offline_event"), Alias::new("id")) + .on_delete(ForeignKeyAction::Cascade) + .to_owned(), + ) + .await?; + + // offline_event_registration.patient_id → patient.id + manager + .create_foreign_key( + ForeignKey::create() + .name("fk_offline_event_registration_patient") + .from(Alias::new("offline_event_registration"), Alias::new("patient_id")) + .to(Alias::new("patient"), Alias::new("id")) + .on_delete(ForeignKeyAction::Cascade) + .to_owned(), + ) + .await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + let fks = [ + "fk_offline_event_registration_patient", + "fk_offline_event_registration_event", + "fk_points_order_patient", + "fk_points_order_product", + "fk_points_transaction_order", + "fk_points_transaction_rule", + "fk_points_transaction_account", + "fk_follow_up_task_appointment", + ]; + for fk in fks { + manager + .drop_foreign_key( + ForeignKey::drop() + .name(fk) + .table(Alias::new("dummy")) + .to_owned(), + ) + .await?; + } + Ok(()) + } +} diff --git a/crates/erp-server/migration/src/m20260504_000110_alter_critical_alerts_version_i32.rs b/crates/erp-server/migration/src/m20260504_000110_alter_critical_alerts_version_i32.rs new file mode 100644 index 0000000..03e6534 --- /dev/null +++ b/crates/erp-server/migration/src/m20260504_000110_alter_critical_alerts_version_i32.rs @@ -0,0 +1,69 @@ +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> { + // critical_alerts.version: i64 → i32 + manager + .alter_table( + Table::alter() + .table(Alias::new("critical_alerts")) + .modify_column( + ColumnDef::new(Alias::new("version")) + .integer() + .not_null() + .default(1), + ) + .to_owned(), + ) + .await?; + + // critical_alert_responses.version: i64 → i32 + manager + .alter_table( + Table::alter() + .table(Alias::new("critical_alert_responses")) + .modify_column( + ColumnDef::new(Alias::new("version")) + .integer() + .not_null() + .default(1), + ) + .to_owned(), + ) + .await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .alter_table( + Table::alter() + .table(Alias::new("critical_alerts")) + .modify_column( + ColumnDef::new(Alias::new("version")) + .big_integer() + .not_null() + .default(1), + ) + .to_owned(), + ) + .await?; + + manager + .alter_table( + Table::alter() + .table(Alias::new("critical_alert_responses")) + .modify_column( + ColumnDef::new(Alias::new("version")) + .big_integer() + .not_null() + .default(1), + ) + .to_owned(), + ) + .await + } +}