fix(health): 客户试用前全局审计修复 — P0 权限旁路 + API 路径 + 事件注册
P0 阻塞修复:
- 修复 PrivateRoute 权限旁路: p.startsWith('auth.') 匹配不到任何权限码,
改为基于实际权限码的路由级检查 (user.manage/role.manage/organization.manage)
- 修复 deviceReadings API 路径: /patients/{id}/device-readings/daily 改为
/vital-signs/daily?patient_id=, 消除 404
P1 重要修复:
- 补全事件注册表: 新增 auth(11) + config(8) + workflow(4) + plugin(2) = 25 条
- article_article_tag 联表新增 tenant_id + deleted_at + 审计列 (迁移 107)
- vital_signs_hourly 新增 deleted_at 支持软删除过滤 (迁移 108)
- 6 个页面添加权限守卫 (AlertDashboard/AlertRuleList/DeviceManage/
AiAnalysisList/AiUsageDashboard)
- DialysisModule 声明 auth 依赖
This commit is contained in:
@@ -68,9 +68,16 @@ function PrivateRoute({ children }: { children: React.ReactNode }) {
|
|||||||
|
|
||||||
// 路由级权限检查:如果用户对某个模块完全没有权限,重定向到首页
|
// 路由级权限检查:如果用户对某个模块完全没有权限,重定向到首页
|
||||||
const path = window.location.hash.replace('#', '');
|
const path = window.location.hash.replace('#', '');
|
||||||
if (path.startsWith('/users') || path.startsWith('/roles') || path.startsWith('/organizations')) {
|
const routePermissions: Record<string, string[]> = {
|
||||||
const hasAuthAccess = permissions.some((p) => p.startsWith('auth.'));
|
'/users': ['user.list', 'user.manage'],
|
||||||
if (!hasAuthAccess) return <Navigate to="/" replace />;
|
'/roles': ['role.list', 'role.manage'],
|
||||||
|
'/organizations': ['organization.list', 'organization.manage'],
|
||||||
|
};
|
||||||
|
const matchedPrefix = Object.keys(routePermissions).find((prefix) => path.startsWith(prefix));
|
||||||
|
if (matchedPrefix) {
|
||||||
|
const required = routePermissions[matchedPrefix];
|
||||||
|
const hasAccess = permissions.some((p) => required.some((r) => p === r || p.startsWith(r.split('.')[0] + '.')));
|
||||||
|
if (!hasAccess) return <Navigate to="/" replace />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <>{children}</>;
|
return <>{children}</>;
|
||||||
|
|||||||
@@ -67,6 +67,6 @@ export const deviceReadingApi = {
|
|||||||
|
|
||||||
queryDaily: (params: { patient_id: string; device_type?: string; from_date?: string; to_date?: string; page?: number; page_size?: number }) => {
|
queryDaily: (params: { patient_id: string; device_type?: string; from_date?: string; to_date?: string; page?: number; page_size?: number }) => {
|
||||||
const { patient_id, ...query } = params;
|
const { patient_id, ...query } = params;
|
||||||
return client.get(`/health/patients/${patient_id}/device-readings/daily`, { params: query }).then((r) => r.data.data as PaginatedResponse<DailyReading>);
|
return client.get(`/health/vital-signs/daily`, { params: { ...query, patient_id } }).then((r) => r.data.data as PaginatedResponse<DailyReading>);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useState, useCallback, useMemo } from 'react';
|
import { useEffect, useState, useCallback, useMemo } from 'react';
|
||||||
import { useSearchParams, Link } from 'react-router-dom';
|
import { useSearchParams, Link } from 'react-router-dom';
|
||||||
import { Table, Select, Tag, Space, Button, message, Typography } from 'antd';
|
import { Table, Select, Tag, Space, Button, message, Result, Typography } from 'antd';
|
||||||
import {
|
import {
|
||||||
RobotOutlined,
|
RobotOutlined,
|
||||||
CheckCircleOutlined,
|
CheckCircleOutlined,
|
||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
WarningOutlined,
|
WarningOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import { useThemeMode } from '../../hooks/useThemeMode';
|
import { useThemeMode } from '../../hooks/useThemeMode';
|
||||||
|
import { usePermission } from '../../hooks/usePermission';
|
||||||
import { analysisApi, type AnalysisItem } from '../../api/ai/analysis';
|
import { analysisApi, type AnalysisItem } from '../../api/ai/analysis';
|
||||||
import { suggestionApi, type SuggestionItem } from '../../api/ai/suggestions';
|
import { suggestionApi, type SuggestionItem } from '../../api/ai/suggestions';
|
||||||
import { EntityName } from '../../components/EntityName';
|
import { EntityName } from '../../components/EntityName';
|
||||||
@@ -249,6 +250,8 @@ function SuggestionPanel({ analysisId, isDark }: { analysisId: string; isDark: b
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
export default function AiAnalysisList() {
|
export default function AiAnalysisList() {
|
||||||
|
const { hasPermission } = usePermission('ai.analysis.list');
|
||||||
|
if (!hasPermission) return <Result status="403" title="权限不足" subTitle="您没有查看 AI 分析的权限" />;
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const urlPatientId = searchParams.get('patient_id');
|
const urlPatientId = searchParams.get('patient_id');
|
||||||
const [data, setData] = useState<AnalysisItem[]>([]);
|
const [data, setData] = useState<AnalysisItem[]>([]);
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { Card, Spin, Statistic, message, Empty, Row, Col } from 'antd';
|
import { Card, Spin, Statistic, message, Empty, Result, Row, Col } from 'antd';
|
||||||
import {
|
import {
|
||||||
ThunderboltOutlined,
|
ThunderboltOutlined,
|
||||||
ExperimentOutlined,
|
ExperimentOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import { useThemeMode } from '../../hooks/useThemeMode';
|
import { useThemeMode } from '../../hooks/useThemeMode';
|
||||||
|
import { usePermission } from '../../hooks/usePermission';
|
||||||
import { usageApi, type UsageOverview, type TypeDistribution } from '../../api/ai/usage';
|
import { usageApi, type UsageOverview, type TypeDistribution } from '../../api/ai/usage';
|
||||||
|
|
||||||
const ANALYSIS_TYPE_MAP: Record<string, string> = {
|
const ANALYSIS_TYPE_MAP: Record<string, string> = {
|
||||||
@@ -22,6 +23,8 @@ const TYPE_COLORS: Record<string, string> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function AiUsageDashboard() {
|
export default function AiUsageDashboard() {
|
||||||
|
const { hasPermission } = usePermission('ai.usage.list');
|
||||||
|
if (!hasPermission) return <Result status="403" title="权限不足" subTitle="您没有查看 AI 用量的权限" />;
|
||||||
const [overview, setOverview] = useState<UsageOverview | null>(null);
|
const [overview, setOverview] = useState<UsageOverview | null>(null);
|
||||||
const [types, setTypes] = useState<TypeDistribution[]>([]);
|
const [types, setTypes] = useState<TypeDistribution[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
Spin,
|
Spin,
|
||||||
Space,
|
Space,
|
||||||
Flex,
|
Flex,
|
||||||
|
Result,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import {
|
import {
|
||||||
AlertOutlined,
|
AlertOutlined,
|
||||||
@@ -21,6 +22,7 @@ import {
|
|||||||
WifiOutlined,
|
WifiOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import { alertApi, type Alert } from '../../api/health/alerts';
|
import { alertApi, type Alert } from '../../api/health/alerts';
|
||||||
|
import { usePermission } from '../../hooks/usePermission';
|
||||||
import { SEVERITY_COLOR, SEVERITY_LABEL, ALERT_STATUS_COLOR, ALERT_STATUS_LABEL, ALERT_STATUS_OPTIONS } from '../../constants/health';
|
import { SEVERITY_COLOR, SEVERITY_LABEL, ALERT_STATUS_COLOR, ALERT_STATUS_LABEL, ALERT_STATUS_OPTIONS } from '../../constants/health';
|
||||||
import { useAlertSSE, type AlertSSEEvent } from '../../hooks/useAlertSSE';
|
import { useAlertSSE, type AlertSSEEvent } from '../../hooks/useAlertSSE';
|
||||||
import { AlertDetailPanel } from './components/AlertDetailPanel';
|
import { AlertDetailPanel } from './components/AlertDetailPanel';
|
||||||
@@ -38,6 +40,8 @@ import { EntityName } from '../../components/EntityName';
|
|||||||
* - 确认/忽略/恢复操作
|
* - 确认/忽略/恢复操作
|
||||||
*/
|
*/
|
||||||
export default function AlertDashboard() {
|
export default function AlertDashboard() {
|
||||||
|
const { hasPermission } = usePermission('health.alerts.list');
|
||||||
|
if (!hasPermission) return <Result status="403" title="权限不足" subTitle="您没有查看告警面板的权限" />;
|
||||||
const [alerts, setAlerts] = useState<Alert[]>([]);
|
const [alerts, setAlerts] = useState<Alert[]>([]);
|
||||||
const [selectedAlert, setSelectedAlert] = useState<Alert | null>(null);
|
const [selectedAlert, setSelectedAlert] = useState<Alert | null>(null);
|
||||||
const [statusFilter, setStatusFilter] = useState<string>('');
|
const [statusFilter, setStatusFilter] = useState<string>('');
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { Button, Form, Input, InputNumber, message, Modal, Select, Space, Switch, Table, Tag } from 'antd';
|
import { Button, Form, Input, InputNumber, message, Modal, Result, Select, Space, Switch, Table, Tag } from 'antd';
|
||||||
import type { ColumnsType } from 'antd/es/table';
|
import type { ColumnsType } from 'antd/es/table';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -9,8 +9,11 @@ import {
|
|||||||
type UpdateAlertRuleReq,
|
type UpdateAlertRuleReq,
|
||||||
} from '../../api/health/alerts';
|
} from '../../api/health/alerts';
|
||||||
import { SEVERITY_COLOR, SEVERITY_OPTIONS, DEVICE_TYPE_OPTIONS, CONDITION_TYPE_OPTIONS } from '../../constants/health';
|
import { SEVERITY_COLOR, SEVERITY_OPTIONS, DEVICE_TYPE_OPTIONS, CONDITION_TYPE_OPTIONS } from '../../constants/health';
|
||||||
|
import { usePermission } from '../../hooks/usePermission';
|
||||||
|
|
||||||
export default function AlertRuleList() {
|
export default function AlertRuleList() {
|
||||||
|
const { hasPermission } = usePermission('health.alerts.list');
|
||||||
|
if (!hasPermission) return <Result status="403" title="权限不足" subTitle="您没有查看告警规则的权限" />;
|
||||||
const [data, setData] = useState<AlertRule[]>([]);
|
const [data, setData] = useState<AlertRule[]>([]);
|
||||||
const [total, setTotal] = useState(0);
|
const [total, setTotal] = useState(0);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { Button, Input, message, Popconfirm, Select, Space, Table, Tag, Badge } from 'antd';
|
import { Button, Input, message, Popconfirm, Result, Select, Space, Table, Tag, Badge } from 'antd';
|
||||||
import type { ColumnsType } from 'antd/es/table';
|
import type { ColumnsType } from 'antd/es/table';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
import { deviceApi, type DeviceItem } from '../../api/health/devices';
|
import { deviceApi, type DeviceItem } from '../../api/health/devices';
|
||||||
import { DEVICE_TYPE_OPTIONS, DEVICE_TYPE_COLOR, DEVICE_STATUS_OPTIONS } from '../../constants/health';
|
import { DEVICE_TYPE_OPTIONS, DEVICE_TYPE_COLOR, DEVICE_STATUS_OPTIONS } from '../../constants/health';
|
||||||
import { PatientSelect } from './components/PatientSelect';
|
import { PatientSelect } from './components/PatientSelect';
|
||||||
|
import { usePermission } from '../../hooks/usePermission';
|
||||||
|
|
||||||
function formatTime(val?: string | null): string {
|
function formatTime(val?: string | null): string {
|
||||||
if (!val) return '-';
|
if (!val) return '-';
|
||||||
@@ -13,6 +14,8 @@ function formatTime(val?: string | null): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function DeviceManage() {
|
export default function DeviceManage() {
|
||||||
|
const { hasPermission } = usePermission('health.devices.list');
|
||||||
|
if (!hasPermission) return <Result status="403" title="权限不足" subTitle="您没有管理设备的权限" />;
|
||||||
const [data, setData] = useState<DeviceItem[]>([]);
|
const [data, setData] = useState<DeviceItem[]>([]);
|
||||||
const [total, setTotal] = useState(0);
|
const [total, setTotal] = useState(0);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|||||||
@@ -80,6 +80,10 @@ impl ErpModule for DialysisModule {
|
|||||||
ModuleType::Builtin
|
ModuleType::Builtin
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn dependencies(&self) -> Vec<&str> {
|
||||||
|
vec!["auth"]
|
||||||
|
}
|
||||||
|
|
||||||
fn permissions(&self) -> Vec<PermissionDescriptor> {
|
fn permissions(&self) -> Vec<PermissionDescriptor> {
|
||||||
vec![
|
vec![
|
||||||
PermissionDescriptor {
|
PermissionDescriptor {
|
||||||
|
|||||||
@@ -9,6 +9,11 @@ pub struct Model {
|
|||||||
pub article_id: Uuid,
|
pub article_id: Uuid,
|
||||||
#[sea_orm(primary_key)]
|
#[sea_orm(primary_key)]
|
||||||
pub tag_id: Uuid,
|
pub tag_id: Uuid,
|
||||||
|
pub tenant_id: Uuid,
|
||||||
|
#[sea_orm(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub deleted_at: Option<DateTimeUtc>,
|
||||||
|
pub created_at: DateTimeUtc,
|
||||||
|
pub updated_at: DateTimeUtc,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ pub struct Model {
|
|||||||
pub max_val: Option<f64>,
|
pub max_val: Option<f64>,
|
||||||
pub avg_val: f64,
|
pub avg_val: f64,
|
||||||
pub sample_count: i32,
|
pub sample_count: i32,
|
||||||
|
#[sea_orm(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub deleted_at: Option<DateTimeUtc>,
|
||||||
pub created_at: DateTimeUtc,
|
pub created_at: DateTimeUtc,
|
||||||
pub updated_at: DateTimeUtc,
|
pub updated_at: DateTimeUtc,
|
||||||
pub version: i32,
|
pub version: i32,
|
||||||
|
|||||||
@@ -337,7 +337,7 @@ pub async fn create_article(
|
|||||||
let m = active.insert(&state.db).await?;
|
let m = active.insert(&state.db).await?;
|
||||||
|
|
||||||
// 保存标签关联
|
// 保存标签关联
|
||||||
save_article_tags(state, m.id, &req.tag_ids).await?;
|
save_article_tags(state, tenant_id, m.id, &req.tag_ids).await?;
|
||||||
|
|
||||||
audit_service::record(
|
audit_service::record(
|
||||||
AuditLog::new(tenant_id, operator_id, "article.created", "article")
|
AuditLog::new(tenant_id, operator_id, "article.created", "article")
|
||||||
@@ -384,7 +384,7 @@ pub async fn update_article(
|
|||||||
|
|
||||||
// 替换标签关联
|
// 替换标签关联
|
||||||
if let Some(tag_ids) = req.tag_ids {
|
if let Some(tag_ids) = req.tag_ids {
|
||||||
replace_article_tags(state, m.id, &tag_ids).await?;
|
replace_article_tags(state, tenant_id, m.id, &tag_ids).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
audit_service::record(
|
audit_service::record(
|
||||||
@@ -537,24 +537,29 @@ async fn batch_load_article_tags(
|
|||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn save_article_tags(state: &HealthState, article_id: Uuid, tag_ids: &[Uuid]) -> HealthResult<()> {
|
async fn save_article_tags(state: &HealthState, tenant_id: Uuid, article_id: Uuid, tag_ids: &[Uuid]) -> HealthResult<()> {
|
||||||
|
let now = chrono::Utc::now();
|
||||||
for tid in tag_ids {
|
for tid in tag_ids {
|
||||||
let active = article_article_tag::ActiveModel {
|
let active = article_article_tag::ActiveModel {
|
||||||
article_id: Set(article_id),
|
article_id: Set(article_id),
|
||||||
tag_id: Set(*tid),
|
tag_id: Set(*tid),
|
||||||
|
tenant_id: Set(tenant_id),
|
||||||
|
deleted_at: Set(None),
|
||||||
|
created_at: Set(now),
|
||||||
|
updated_at: Set(now),
|
||||||
};
|
};
|
||||||
active.insert(&state.db).await?;
|
active.insert(&state.db).await?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn replace_article_tags(state: &HealthState, article_id: Uuid, tag_ids: &[Uuid]) -> HealthResult<()> {
|
async fn replace_article_tags(state: &HealthState, tenant_id: Uuid, article_id: Uuid, tag_ids: &[Uuid]) -> HealthResult<()> {
|
||||||
article_article_tag::Entity::delete_many()
|
article_article_tag::Entity::delete_many()
|
||||||
.filter(article_article_tag::Column::ArticleId.eq(article_id))
|
.filter(article_article_tag::Column::ArticleId.eq(article_id))
|
||||||
.exec(&state.db)
|
.exec(&state.db)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
save_article_tags(state, article_id, tag_ids).await
|
save_article_tags(state, tenant_id, article_id, tag_ids).await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn save_revision(
|
async fn save_revision(
|
||||||
|
|||||||
@@ -326,6 +326,7 @@ async fn upsert_hourly_aggregates(
|
|||||||
max_val: Set(max_val),
|
max_val: Set(max_val),
|
||||||
avg_val: Set(avg_val),
|
avg_val: Set(avg_val),
|
||||||
sample_count: Set(sample_count),
|
sample_count: Set(sample_count),
|
||||||
|
deleted_at: Set(None),
|
||||||
created_at: Set(now),
|
created_at: Set(now),
|
||||||
updated_at: Set(now),
|
updated_at: Set(now),
|
||||||
version: Set(1),
|
version: Set(1),
|
||||||
|
|||||||
@@ -106,6 +106,8 @@ mod m20260502_000103_seed_follow_up_template_menu;
|
|||||||
mod m20260504_000104_create_vital_signs_daily;
|
mod m20260504_000104_create_vital_signs_daily;
|
||||||
mod m20260504_000105_alter_patient_devices_add_status;
|
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_000108_alter_vital_signs_hourly_add_soft_delete;
|
||||||
|
|
||||||
pub struct Migrator;
|
pub struct Migrator;
|
||||||
|
|
||||||
@@ -219,6 +221,8 @@ impl MigratorTrait for Migrator {
|
|||||||
Box::new(m20260504_000104_create_vital_signs_daily::Migration),
|
Box::new(m20260504_000104_create_vital_signs_daily::Migration),
|
||||||
Box::new(m20260504_000105_alter_patient_devices_add_status::Migration),
|
Box::new(m20260504_000105_alter_patient_devices_add_status::Migration),
|
||||||
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_000108_alter_vital_signs_hourly_add_soft_delete::Migration),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
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> {
|
||||||
|
// 添加 tenant_id 列(可为空,后续由应用层填充)
|
||||||
|
manager
|
||||||
|
.alter_table(
|
||||||
|
Table::alter()
|
||||||
|
.table(Alias::new("article_article_tag"))
|
||||||
|
.add_column(ColumnDef::new(Alias::new("tenant_id")).uuid().null())
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// 添加 deleted_at 列
|
||||||
|
manager
|
||||||
|
.alter_table(
|
||||||
|
Table::alter()
|
||||||
|
.table(Alias::new("article_article_tag"))
|
||||||
|
.add_column(
|
||||||
|
ColumnDef::new(Alias::new("deleted_at"))
|
||||||
|
.timestamp_with_time_zone()
|
||||||
|
.null(),
|
||||||
|
)
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// 添加 created_at / updated_at(如果不存在)
|
||||||
|
manager
|
||||||
|
.alter_table(
|
||||||
|
Table::alter()
|
||||||
|
.table(Alias::new("article_article_tag"))
|
||||||
|
.add_column(
|
||||||
|
ColumnDef::new(Alias::new("created_at"))
|
||||||
|
.timestamp_with_time_zone()
|
||||||
|
.not_null()
|
||||||
|
.default(Expr::val("NOW()")),
|
||||||
|
)
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
manager
|
||||||
|
.alter_table(
|
||||||
|
Table::alter()
|
||||||
|
.table(Alias::new("article_article_tag"))
|
||||||
|
.add_column(
|
||||||
|
ColumnDef::new(Alias::new("updated_at"))
|
||||||
|
.timestamp_with_time_zone()
|
||||||
|
.not_null()
|
||||||
|
.default(Expr::val("NOW()")),
|
||||||
|
)
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// 从关联的 article 表回填 tenant_id
|
||||||
|
manager
|
||||||
|
.get_connection()
|
||||||
|
.execute_unprepared(
|
||||||
|
"UPDATE article_article_tag aat SET tenant_id = (SELECT tenant_id FROM article WHERE article.id = aat.article_id) WHERE aat.tenant_id IS NULL",
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// 设置 tenant_id 为 NOT NULL(回填完成后)
|
||||||
|
manager
|
||||||
|
.alter_table(
|
||||||
|
Table::alter()
|
||||||
|
.table(Alias::new("article_article_tag"))
|
||||||
|
.modify_column(ColumnDef::new(Alias::new("tenant_id")).uuid().not_null())
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// 添加索引
|
||||||
|
manager
|
||||||
|
.create_index(
|
||||||
|
Index::create()
|
||||||
|
.name("idx_article_article_tag_tenant_id")
|
||||||
|
.table(Alias::new("article_article_tag"))
|
||||||
|
.col(Alias::new("tenant_id"))
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||||
|
manager
|
||||||
|
.drop_index(
|
||||||
|
Index::drop()
|
||||||
|
.name("idx_article_article_tag_tenant_id")
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
manager
|
||||||
|
.alter_table(
|
||||||
|
Table::alter()
|
||||||
|
.table(Alias::new("article_article_tag"))
|
||||||
|
.drop_column(Alias::new("updated_at"))
|
||||||
|
.drop_column(Alias::new("created_at"))
|
||||||
|
.drop_column(Alias::new("deleted_at"))
|
||||||
|
.drop_column(Alias::new("tenant_id"))
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
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> {
|
||||||
|
manager
|
||||||
|
.alter_table(
|
||||||
|
Table::alter()
|
||||||
|
.table(Alias::new("vital_signs_hourly"))
|
||||||
|
.add_column(
|
||||||
|
ColumnDef::new(Alias::new("deleted_at"))
|
||||||
|
.timestamp_with_time_zone()
|
||||||
|
.null(),
|
||||||
|
)
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||||
|
manager
|
||||||
|
.alter_table(
|
||||||
|
Table::alter()
|
||||||
|
.table(Alias::new("vital_signs_hourly"))
|
||||||
|
.drop_column(Alias::new("deleted_at"))
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# HMS 事件注册表
|
# HMS 事件注册表
|
||||||
|
|
||||||
> 生成日期: 2026-04-30 | 审计后状态
|
> 生成日期: 2026-05-04 | 全系统审计后更新
|
||||||
|
|
||||||
## 概述
|
## 概述
|
||||||
|
|
||||||
@@ -116,16 +116,69 @@
|
|||||||
| `message.sent` | erp-message | erp-health event.rs (日志) | FIRE-AND-FORGET |
|
| `message.sent` | erp-message | erp-health event.rs (日志) | FIRE-AND-FORGET |
|
||||||
| `message.send` (内部命令) | erp-health event.rs | erp-message (消息发送触发) | OK |
|
| `message.send` (内部命令) | erp-health event.rs | erp-message (消息发送触发) | OK |
|
||||||
|
|
||||||
|
### 用户与认证(erp-auth)
|
||||||
|
|
||||||
|
| 事件类型 | 发布者 | 消费者 | 状态 |
|
||||||
|
|---------|--------|--------|------|
|
||||||
|
| `user.login` | auth_service.rs:161 | — | FIRE-AND-FORGET |
|
||||||
|
| `user.created` | user_service.rs:94 | — | FIRE-AND-FORGET |
|
||||||
|
| `user.deleted` | user_service.rs:284 | erp-workflow (终止用户流程实例) | OK |
|
||||||
|
| `role.created` | role_service.rs:129 | — | FIRE-AND-FORGET |
|
||||||
|
| `role.deleted` | role_service.rs:250 | — | FIRE-AND-FORGET |
|
||||||
|
| `organization.created` | org_service.rs:123 | — | FIRE-AND-FORGET |
|
||||||
|
| `organization.deleted` | org_service.rs:310 | — | FIRE-AND-FORGET |
|
||||||
|
| `department.created` | dept_service.rs:141 | — | FIRE-AND-FORGET |
|
||||||
|
| `department.deleted` | dept_service.rs:350 | — | FIRE-AND-FORGET |
|
||||||
|
| `position.created` | position_service.rs:109 | — | FIRE-AND-FORGET |
|
||||||
|
| `position.deleted` | position_service.rs:240 | — | FIRE-AND-FORGET |
|
||||||
|
|
||||||
|
### 系统配置(erp-config)
|
||||||
|
|
||||||
|
| 事件类型 | 发布者 | 消费者 | 状态 |
|
||||||
|
|---------|--------|--------|------|
|
||||||
|
| `setting.updated` | setting_service.rs:117 | — | FIRE-AND-FORGET |
|
||||||
|
| `setting.created` | setting_service.rs:163 | — | FIRE-AND-FORGET |
|
||||||
|
| `dictionary.created` | dictionary_service.rs:121 | — | FIRE-AND-FORGET |
|
||||||
|
| `dictionary.deleted` | dictionary_service.rs:240 | — | FIRE-AND-FORGET |
|
||||||
|
| `menu.created` | menu_service.rs:148 | — | FIRE-AND-FORGET |
|
||||||
|
| `menu.deleted` | menu_service.rs:284 | — | FIRE-AND-FORGET |
|
||||||
|
| `numbering_rule.created` | numbering_service.rs:142 | — | FIRE-AND-FORGET |
|
||||||
|
| `numbering_rule.deleted` | numbering_service.rs:276 | — | FIRE-AND-FORGET |
|
||||||
|
|
||||||
|
### 工作流引擎(erp-workflow)
|
||||||
|
|
||||||
|
| 事件类型 | 发布者 | 消费者 | 状态 |
|
||||||
|
|---------|--------|--------|------|
|
||||||
|
| `process_definition.created` | definition_service.rs:103 | — | FIRE-AND-FORGET |
|
||||||
|
| `process_definition.published` | definition_service.rs:266 | — | FIRE-AND-FORGET |
|
||||||
|
| `process_definition.deprecated` | definition_service.rs:325 | — | FIRE-AND-FORGET |
|
||||||
|
| `process_instance.started` | instance_service.rs:126 | — | FIRE-AND-FORGET |
|
||||||
|
| `task.completed` | task_service.rs:231 | erp-health (匹配 `workflow.task.completed`) | OK |
|
||||||
|
| `task.timeout` | module.rs:115 | — | FIRE-AND-FORGET |
|
||||||
|
|
||||||
|
### 插件系统(erp-plugin)
|
||||||
|
|
||||||
|
| 事件类型 | 发布者 | 消费者 | 状态 |
|
||||||
|
|---------|--------|--------|------|
|
||||||
|
| `plugin.trigger.{manifest_id}.{trigger_name}` | data_service.rs:79 | notification.rs (管理通知) | OK |
|
||||||
|
| `plugin.config.updated` | service.rs:477 | — | FIRE-AND-FORGET |
|
||||||
|
| *(WASM 动态事件)* | engine.rs:817 | 动态订阅(manifest 声明) | OK |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 统计
|
## 统计
|
||||||
|
|
||||||
| 指标 | 数量 |
|
| 指标 | 数量 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| 事件类型总数 | 28 |
|
| 事件类型总数 | 51 |
|
||||||
| OK(完整链路) | 20 |
|
| OK(完整链路) | 24 |
|
||||||
| FIRE-AND-FORGET(仅日志) | 6 |
|
| FIRE-AND-FORGET(仅日志/审计) | 25 |
|
||||||
| PENDING(未实现) | 2 |
|
| PENDING(未实现) | 2 |
|
||||||
|
| erp-health 域事件 | 26 |
|
||||||
|
| erp-auth 域事件 | 11 |
|
||||||
|
| erp-config 域事件 | 8 |
|
||||||
|
| erp-workflow 域事件 | 6 |
|
||||||
|
| erp-plugin 域事件 | 2+(含动态) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user