From 444dc7dd8d0013141a2ba95acefcffd3de61661a Mon Sep 17 00:00:00 2001 From: iven Date: Mon, 4 May 2026 11:22:54 +0800 Subject: [PATCH] =?UTF-8?q?fix(health):=20=E6=95=B0=E6=8D=AE=E5=AE=8C?= =?UTF-8?q?=E6=95=B4=E6=80=A7=20+=20=E4=BB=A3=E7=A0=81=E8=A7=84=E8=8C=83?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20=E2=80=94=20FK=E7=BA=A6=E6=9D=9F/=E7=89=88?= =?UTF-8?q?=E6=9C=AC=E7=B1=BB=E5=9E=8B=E7=BB=9F=E4=B8=80/=E8=BD=AF?= =?UTF-8?q?=E5=88=A0=E9=99=A4=E8=BF=87=E6=BB=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 数据完整性: - 新增 8 个 FK 约束 (follow_up_task→appointment, points_transaction→account/rule/order, points_order→product/patient, offline_event_registration→event/patient) - critical_alert/critical_alert_response version 字段 i64→i32 统一 - vital_signs_daily_service 聚合查询添加 DeletedAt.is_null() 过滤 代码规范: - 新增 api/upload.ts 封装文件上传,ArticleEditor 改用 service 层 - 新增 messages.updateSubscription,NotificationPreferences 改用 service 层 - 修复 erp-message SSE 测试编译错误 (移除 serde_urlencoded 依赖) --- apps/web/src/api/messages.ts | 10 ++ apps/web/src/api/upload.ts | 16 +++ apps/web/src/pages/health/ArticleEditor.tsx | 15 +- .../messages/NotificationPreferences.tsx | 4 +- .../erp-health/src/entity/critical_alert.rs | 2 +- .../src/entity/critical_alert_response.rs | 2 +- .../src/service/vital_signs_daily_service.rs | 2 + crates/erp-message/src/handler/sse_handler.rs | 12 +- crates/erp-server/migration/src/lib.rs | 4 + ...60504_000109_add_missing_fk_constraints.rs | 129 ++++++++++++++++++ ...00110_alter_critical_alerts_version_i32.rs | 69 ++++++++++ 11 files changed, 245 insertions(+), 20 deletions(-) create mode 100644 apps/web/src/api/upload.ts create mode 100644 crates/erp-server/migration/src/m20260504_000109_add_missing_fk_constraints.rs create mode 100644 crates/erp-server/migration/src/m20260504_000110_alter_critical_alerts_version_i32.rs 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 + } +}