fix(health): 数据完整性 + 代码规范修复 — FK约束/版本类型统一/软删除过滤
数据完整性: - 新增 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 依赖)
This commit is contained in:
@@ -86,3 +86,13 @@ export async function sendMessage(req: SendMessageRequest) {
|
|||||||
);
|
);
|
||||||
return data.data;
|
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);
|
||||||
|
}
|
||||||
|
|||||||
16
apps/web/src/api/upload.ts
Normal file
16
apps/web/src/api/upload.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import client from './client';
|
||||||
|
|
||||||
|
export interface UploadResult {
|
||||||
|
url: string;
|
||||||
|
filename?: string;
|
||||||
|
size?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function uploadFile(file: File): Promise<UploadResult> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
import { useThemeMode } from '../../hooks/useThemeMode';
|
import { useThemeMode } from '../../hooks/useThemeMode';
|
||||||
import { AuthButton } from '../../components/AuthButton';
|
import { AuthButton } from '../../components/AuthButton';
|
||||||
import client, { handleApiError } from '../../api/client';
|
import client, { handleApiError } from '../../api/client';
|
||||||
|
import { uploadFile } from '../../api/upload';
|
||||||
import '@wangeditor/editor/dist/css/style.css';
|
import '@wangeditor/editor/dist/css/style.css';
|
||||||
|
|
||||||
export default function ArticleEditor() {
|
export default function ArticleEditor() {
|
||||||
@@ -109,10 +110,8 @@ export default function ArticleEditor() {
|
|||||||
try {
|
try {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
const { data: result } = await client.post('/upload', formData, {
|
const result = await uploadFile(file);
|
||||||
headers: { 'Content-Type': 'multipart/form-data' },
|
const url: string = result.url;
|
||||||
});
|
|
||||||
const url: string = result.data.url;
|
|
||||||
const token = localStorage.getItem('access_token');
|
const token = localStorage.getItem('access_token');
|
||||||
const urlWithToken = token ? `${url}?token=${token}` : url;
|
const urlWithToken = token ? `${url}?token=${token}` : url;
|
||||||
insertFn(urlWithToken, file.name, urlWithToken);
|
insertFn(urlWithToken, file.name, urlWithToken);
|
||||||
@@ -464,12 +463,8 @@ export default function ArticleEditor() {
|
|||||||
showUploadList={false}
|
showUploadList={false}
|
||||||
beforeUpload={async (file) => {
|
beforeUpload={async (file) => {
|
||||||
try {
|
try {
|
||||||
const formData = new FormData();
|
const result = await uploadFile(file);
|
||||||
formData.append('file', file);
|
setCoverImage(result.url);
|
||||||
const { data: result } = await client.post('/upload', formData, {
|
|
||||||
headers: { 'Content-Type': 'multipart/form-data' },
|
|
||||||
});
|
|
||||||
setCoverImage(result.data.url);
|
|
||||||
message.success('封面图上传成功');
|
message.success('封面图上传成功');
|
||||||
} catch {
|
} catch {
|
||||||
message.error('封面图上传失败');
|
message.error('封面图上传失败');
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { Form, Switch, TimePicker, Button, message } from 'antd';
|
import { Form, Switch, TimePicker, Button, message } from 'antd';
|
||||||
import { BellOutlined } from '@ant-design/icons';
|
import { BellOutlined } from '@ant-design/icons';
|
||||||
import client from '../../api/client';
|
|
||||||
import { useThemeMode } from '../../hooks/useThemeMode';
|
import { useThemeMode } from '../../hooks/useThemeMode';
|
||||||
|
import { updateSubscription } from '../../api/messages';
|
||||||
|
|
||||||
interface PreferencesData {
|
interface PreferencesData {
|
||||||
dnd_enabled: boolean;
|
dnd_enabled: boolean;
|
||||||
@@ -38,7 +38,7 @@ export default function NotificationPreferences() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await client.put('/message-subscriptions', {
|
await updateSubscription({
|
||||||
dnd_enabled: req.dnd_enabled,
|
dnd_enabled: req.dnd_enabled,
|
||||||
dnd_start: req.dnd_start,
|
dnd_start: req.dnd_start,
|
||||||
dnd_end: req.dnd_end,
|
dnd_end: req.dnd_end,
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ pub struct Model {
|
|||||||
pub updated_by: Option<Uuid>,
|
pub updated_by: Option<Uuid>,
|
||||||
#[sea_orm(skip_serializing_if = "Option::is_none")]
|
#[sea_orm(skip_serializing_if = "Option::is_none")]
|
||||||
pub deleted_at: Option<DateTimeUtc>,
|
pub deleted_at: Option<DateTimeUtc>,
|
||||||
pub version: i64,
|
pub version: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ pub struct Model {
|
|||||||
pub updated_by: Option<Uuid>,
|
pub updated_by: Option<Uuid>,
|
||||||
#[sea_orm(skip_serializing_if = "Option::is_none")]
|
#[sea_orm(skip_serializing_if = "Option::is_none")]
|
||||||
pub deleted_at: Option<DateTimeUtc>,
|
pub deleted_at: Option<DateTimeUtc>,
|
||||||
pub version: i64,
|
pub version: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||||
|
|||||||
@@ -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::TenantId.eq(tenant_id))
|
||||||
.filter(crate::entity::vital_signs_hourly::Column::HourStart.gte(start_of_day))
|
.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::HourStart.lte(end_of_day))
|
||||||
|
.filter(crate::entity::vital_signs_hourly::Column::DeletedAt.is_null())
|
||||||
.all(db)
|
.all(db)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
@@ -94,6 +95,7 @@ pub async fn aggregate_daily_for_all_tenants(
|
|||||||
let hourly_rows = crate::entity::vital_signs_hourly::Entity::find()
|
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.gte(start_of_day))
|
||||||
.filter(crate::entity::vital_signs_hourly::Column::HourStart.lte(end_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)
|
.all(db)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
|||||||
@@ -251,7 +251,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn sse_query_parses_patient_ids() {
|
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());
|
assert!(query.patient_ids.is_some());
|
||||||
let ids = query.patient_ids.unwrap();
|
let ids = query.patient_ids.unwrap();
|
||||||
assert_eq!(ids, "id1,id2,id3");
|
assert_eq!(ids, "id1,id2,id3");
|
||||||
@@ -259,17 +259,17 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn sse_query_default_is_empty() {
|
fn sse_query_default_is_empty() {
|
||||||
let query: SseQuery = serde_urlencoded::from_str("").unwrap();
|
let query = SseQuery::default();
|
||||||
assert!(query.patient_ids.is_none());
|
assert!(query.patient_ids.is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn subscribed_patient_ids_parsing() {
|
fn subscribed_patient_ids_parsing() {
|
||||||
let query: SseQuery = serde_urlencoded::from_str("patient_ids=aaa,bbb,ccc").unwrap();
|
let query = SseQuery { patient_ids: Some("aaa,bbb,ccc".into()) };
|
||||||
let set: Option<HashSet<String>> = query.patient_ids.map(|s| {
|
let set: Option<HashSet<String>> = query.patient_ids.map(|s: String| {
|
||||||
s.split(',')
|
s.split(',')
|
||||||
.map(|id| id.trim().to_string())
|
.map(|id: &str| id.trim().to_string())
|
||||||
.filter(|id| !id.is_empty())
|
.filter(|id: &String| !id.is_empty())
|
||||||
.collect()
|
.collect()
|
||||||
});
|
});
|
||||||
assert!(set.is_some());
|
assert!(set.is_some());
|
||||||
|
|||||||
@@ -108,6 +108,8 @@ mod m20260504_000105_alter_patient_devices_add_status;
|
|||||||
mod m20260504_000106_create_api_clients;
|
mod m20260504_000106_create_api_clients;
|
||||||
mod m20260504_000107_alter_article_article_tag_add_tenant_and_soft_delete;
|
mod m20260504_000107_alter_article_article_tag_add_tenant_and_soft_delete;
|
||||||
mod m20260504_000108_alter_vital_signs_hourly_add_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;
|
pub struct Migrator;
|
||||||
|
|
||||||
@@ -223,6 +225,8 @@ impl MigratorTrait for Migrator {
|
|||||||
Box::new(m20260504_000106_create_api_clients::Migration),
|
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_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_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),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user