From 30a578ee00b87d258cb0bd022f0a1cf03cd5d2d5 Mon Sep 17 00:00:00 2001 From: iven Date: Mon, 4 May 2026 11:02:25 +0800 Subject: [PATCH] =?UTF-8?q?fix(health):=20=E5=AE=A2=E6=88=B7=E8=AF=95?= =?UTF-8?q?=E7=94=A8=E5=89=8D=E5=85=A8=E5=B1=80=E5=AE=A1=E8=AE=A1=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=20=E2=80=94=20P0=20=E6=9D=83=E9=99=90=E6=97=81?= =?UTF-8?q?=E8=B7=AF=20+=20API=20=E8=B7=AF=E5=BE=84=20+=20=E4=BA=8B?= =?UTF-8?q?=E4=BB=B6=E6=B3=A8=E5=86=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 依赖 --- apps/web/src/App.tsx | 13 +- apps/web/src/api/health/deviceReadings.ts | 2 +- apps/web/src/pages/health/AiAnalysisList.tsx | 5 +- .../web/src/pages/health/AiUsageDashboard.tsx | 5 +- apps/web/src/pages/health/AlertDashboard.tsx | 4 + apps/web/src/pages/health/AlertRuleList.tsx | 5 +- apps/web/src/pages/health/DeviceManage.tsx | 5 +- crates/erp-dialysis/src/module.rs | 4 + .../src/entity/article_article_tag.rs | 5 + .../src/entity/vital_signs_hourly.rs | 2 + .../erp-health/src/service/article_service.rs | 15 ++- .../src/service/device_reading_service.rs | 1 + crates/erp-server/migration/src/lib.rs | 4 + ..._article_tag_add_tenant_and_soft_delete.rs | 113 ++++++++++++++++++ ...lter_vital_signs_hourly_add_soft_delete.rs | 33 +++++ docs/event-registry.md | 61 +++++++++- 16 files changed, 260 insertions(+), 17 deletions(-) create mode 100644 crates/erp-server/migration/src/m20260504_000107_alter_article_article_tag_add_tenant_and_soft_delete.rs create mode 100644 crates/erp-server/migration/src/m20260504_000108_alter_vital_signs_hourly_add_soft_delete.rs diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index f49cb36..5d8d79b 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -68,9 +68,16 @@ function PrivateRoute({ children }: { children: React.ReactNode }) { // 路由级权限检查:如果用户对某个模块完全没有权限,重定向到首页 const path = window.location.hash.replace('#', ''); - if (path.startsWith('/users') || path.startsWith('/roles') || path.startsWith('/organizations')) { - const hasAuthAccess = permissions.some((p) => p.startsWith('auth.')); - if (!hasAuthAccess) return ; + const routePermissions: Record = { + '/users': ['user.list', 'user.manage'], + '/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 ; } return <>{children}; diff --git a/apps/web/src/api/health/deviceReadings.ts b/apps/web/src/api/health/deviceReadings.ts index 403faff..94f0424 100644 --- a/apps/web/src/api/health/deviceReadings.ts +++ b/apps/web/src/api/health/deviceReadings.ts @@ -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 }) => { const { patient_id, ...query } = params; - return client.get(`/health/patients/${patient_id}/device-readings/daily`, { params: query }).then((r) => r.data.data as PaginatedResponse); + return client.get(`/health/vital-signs/daily`, { params: { ...query, patient_id } }).then((r) => r.data.data as PaginatedResponse); }, }; diff --git a/apps/web/src/pages/health/AiAnalysisList.tsx b/apps/web/src/pages/health/AiAnalysisList.tsx index d93861c..f1dc522 100644 --- a/apps/web/src/pages/health/AiAnalysisList.tsx +++ b/apps/web/src/pages/health/AiAnalysisList.tsx @@ -1,6 +1,6 @@ import { useEffect, useState, useCallback, useMemo } from 'react'; 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 { RobotOutlined, CheckCircleOutlined, @@ -9,6 +9,7 @@ import { WarningOutlined, } from '@ant-design/icons'; import { useThemeMode } from '../../hooks/useThemeMode'; +import { usePermission } from '../../hooks/usePermission'; import { analysisApi, type AnalysisItem } from '../../api/ai/analysis'; import { suggestionApi, type SuggestionItem } from '../../api/ai/suggestions'; import { EntityName } from '../../components/EntityName'; @@ -249,6 +250,8 @@ function SuggestionPanel({ analysisId, isDark }: { analysisId: string; isDark: b // --------------------------------------------------------------------------- export default function AiAnalysisList() { + const { hasPermission } = usePermission('ai.analysis.list'); + if (!hasPermission) return ; const [searchParams] = useSearchParams(); const urlPatientId = searchParams.get('patient_id'); const [data, setData] = useState([]); diff --git a/apps/web/src/pages/health/AiUsageDashboard.tsx b/apps/web/src/pages/health/AiUsageDashboard.tsx index 779f1dc..8ed9e61 100644 --- a/apps/web/src/pages/health/AiUsageDashboard.tsx +++ b/apps/web/src/pages/health/AiUsageDashboard.tsx @@ -1,10 +1,11 @@ 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 { ThunderboltOutlined, ExperimentOutlined, } from '@ant-design/icons'; import { useThemeMode } from '../../hooks/useThemeMode'; +import { usePermission } from '../../hooks/usePermission'; import { usageApi, type UsageOverview, type TypeDistribution } from '../../api/ai/usage'; const ANALYSIS_TYPE_MAP: Record = { @@ -22,6 +23,8 @@ const TYPE_COLORS: Record = { }; export default function AiUsageDashboard() { + const { hasPermission } = usePermission('ai.usage.list'); + if (!hasPermission) return ; const [overview, setOverview] = useState(null); const [types, setTypes] = useState([]); const [loading, setLoading] = useState(true); diff --git a/apps/web/src/pages/health/AlertDashboard.tsx b/apps/web/src/pages/health/AlertDashboard.tsx index 330a8ac..d8830a6 100644 --- a/apps/web/src/pages/health/AlertDashboard.tsx +++ b/apps/web/src/pages/health/AlertDashboard.tsx @@ -12,6 +12,7 @@ import { Spin, Space, Flex, + Result, } from 'antd'; import { AlertOutlined, @@ -21,6 +22,7 @@ import { WifiOutlined, } from '@ant-design/icons'; 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 { useAlertSSE, type AlertSSEEvent } from '../../hooks/useAlertSSE'; import { AlertDetailPanel } from './components/AlertDetailPanel'; @@ -38,6 +40,8 @@ import { EntityName } from '../../components/EntityName'; * - 确认/忽略/恢复操作 */ export default function AlertDashboard() { + const { hasPermission } = usePermission('health.alerts.list'); + if (!hasPermission) return ; const [alerts, setAlerts] = useState([]); const [selectedAlert, setSelectedAlert] = useState(null); const [statusFilter, setStatusFilter] = useState(''); diff --git a/apps/web/src/pages/health/AlertRuleList.tsx b/apps/web/src/pages/health/AlertRuleList.tsx index f0a8320..90dfa03 100644 --- a/apps/web/src/pages/health/AlertRuleList.tsx +++ b/apps/web/src/pages/health/AlertRuleList.tsx @@ -1,5 +1,5 @@ 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 { @@ -9,8 +9,11 @@ import { type UpdateAlertRuleReq, } from '../../api/health/alerts'; import { SEVERITY_COLOR, SEVERITY_OPTIONS, DEVICE_TYPE_OPTIONS, CONDITION_TYPE_OPTIONS } from '../../constants/health'; +import { usePermission } from '../../hooks/usePermission'; export default function AlertRuleList() { + const { hasPermission } = usePermission('health.alerts.list'); + if (!hasPermission) return ; const [data, setData] = useState([]); const [total, setTotal] = useState(0); const [loading, setLoading] = useState(false); diff --git a/apps/web/src/pages/health/DeviceManage.tsx b/apps/web/src/pages/health/DeviceManage.tsx index c054ff8..199dd82 100644 --- a/apps/web/src/pages/health/DeviceManage.tsx +++ b/apps/web/src/pages/health/DeviceManage.tsx @@ -1,11 +1,12 @@ 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 dayjs from 'dayjs'; import { deviceApi, type DeviceItem } from '../../api/health/devices'; import { DEVICE_TYPE_OPTIONS, DEVICE_TYPE_COLOR, DEVICE_STATUS_OPTIONS } from '../../constants/health'; import { PatientSelect } from './components/PatientSelect'; +import { usePermission } from '../../hooks/usePermission'; function formatTime(val?: string | null): string { if (!val) return '-'; @@ -13,6 +14,8 @@ function formatTime(val?: string | null): string { } export default function DeviceManage() { + const { hasPermission } = usePermission('health.devices.list'); + if (!hasPermission) return ; const [data, setData] = useState([]); const [total, setTotal] = useState(0); const [loading, setLoading] = useState(false); diff --git a/crates/erp-dialysis/src/module.rs b/crates/erp-dialysis/src/module.rs index 9c51b1d..60dbbcc 100644 --- a/crates/erp-dialysis/src/module.rs +++ b/crates/erp-dialysis/src/module.rs @@ -80,6 +80,10 @@ impl ErpModule for DialysisModule { ModuleType::Builtin } + fn dependencies(&self) -> Vec<&str> { + vec!["auth"] + } + fn permissions(&self) -> Vec { vec![ PermissionDescriptor { diff --git a/crates/erp-health/src/entity/article_article_tag.rs b/crates/erp-health/src/entity/article_article_tag.rs index af9e089..6c916b2 100644 --- a/crates/erp-health/src/entity/article_article_tag.rs +++ b/crates/erp-health/src/entity/article_article_tag.rs @@ -9,6 +9,11 @@ pub struct Model { pub article_id: Uuid, #[sea_orm(primary_key)] pub tag_id: Uuid, + pub tenant_id: Uuid, + #[sea_orm(skip_serializing_if = "Option::is_none")] + pub deleted_at: Option, + pub created_at: DateTimeUtc, + pub updated_at: DateTimeUtc, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] diff --git a/crates/erp-health/src/entity/vital_signs_hourly.rs b/crates/erp-health/src/entity/vital_signs_hourly.rs index d8225f3..2cae155 100644 --- a/crates/erp-health/src/entity/vital_signs_hourly.rs +++ b/crates/erp-health/src/entity/vital_signs_hourly.rs @@ -16,6 +16,8 @@ pub struct Model { pub max_val: Option, pub avg_val: f64, pub sample_count: i32, + #[sea_orm(skip_serializing_if = "Option::is_none")] + pub deleted_at: Option, pub created_at: DateTimeUtc, pub updated_at: DateTimeUtc, pub version: i32, diff --git a/crates/erp-health/src/service/article_service.rs b/crates/erp-health/src/service/article_service.rs index 0bbca01..fb9d41f 100644 --- a/crates/erp-health/src/service/article_service.rs +++ b/crates/erp-health/src/service/article_service.rs @@ -337,7 +337,7 @@ pub async fn create_article( 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( 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 { - replace_article_tags(state, m.id, &tag_ids).await?; + replace_article_tags(state, tenant_id, m.id, &tag_ids).await?; } audit_service::record( @@ -537,24 +537,29 @@ async fn batch_load_article_tags( 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 { let active = article_article_tag::ActiveModel { article_id: Set(article_id), 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?; } 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() .filter(article_article_tag::Column::ArticleId.eq(article_id)) .exec(&state.db) .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( diff --git a/crates/erp-health/src/service/device_reading_service.rs b/crates/erp-health/src/service/device_reading_service.rs index e42f02b..7a4201e 100644 --- a/crates/erp-health/src/service/device_reading_service.rs +++ b/crates/erp-health/src/service/device_reading_service.rs @@ -326,6 +326,7 @@ async fn upsert_hourly_aggregates( max_val: Set(max_val), avg_val: Set(avg_val), sample_count: Set(sample_count), + deleted_at: Set(None), created_at: Set(now), updated_at: Set(now), version: Set(1), diff --git a/crates/erp-server/migration/src/lib.rs b/crates/erp-server/migration/src/lib.rs index 7600831..575f4b7 100644 --- a/crates/erp-server/migration/src/lib.rs +++ b/crates/erp-server/migration/src/lib.rs @@ -106,6 +106,8 @@ mod m20260502_000103_seed_follow_up_template_menu; mod m20260504_000104_create_vital_signs_daily; 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; pub struct Migrator; @@ -219,6 +221,8 @@ impl MigratorTrait for Migrator { Box::new(m20260504_000104_create_vital_signs_daily::Migration), Box::new(m20260504_000105_alter_patient_devices_add_status::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), ] } } diff --git a/crates/erp-server/migration/src/m20260504_000107_alter_article_article_tag_add_tenant_and_soft_delete.rs b/crates/erp-server/migration/src/m20260504_000107_alter_article_article_tag_add_tenant_and_soft_delete.rs new file mode 100644 index 0000000..5ac59f1 --- /dev/null +++ b/crates/erp-server/migration/src/m20260504_000107_alter_article_article_tag_add_tenant_and_soft_delete.rs @@ -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 + } +} diff --git a/crates/erp-server/migration/src/m20260504_000108_alter_vital_signs_hourly_add_soft_delete.rs b/crates/erp-server/migration/src/m20260504_000108_alter_vital_signs_hourly_add_soft_delete.rs new file mode 100644 index 0000000..8c74378 --- /dev/null +++ b/crates/erp-server/migration/src/m20260504_000108_alter_vital_signs_hourly_add_soft_delete.rs @@ -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 + } +} diff --git a/docs/event-registry.md b/docs/event-registry.md index 4209154..c81c1fa 100644 --- a/docs/event-registry.md +++ b/docs/event-registry.md @@ -1,6 +1,6 @@ # HMS 事件注册表 -> 生成日期: 2026-04-30 | 审计后状态 +> 生成日期: 2026-05-04 | 全系统审计后更新 ## 概述 @@ -116,16 +116,69 @@ | `message.sent` | erp-message | erp-health event.rs (日志) | FIRE-AND-FORGET | | `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 | -| OK(完整链路) | 20 | -| FIRE-AND-FORGET(仅日志) | 6 | +| 事件类型总数 | 51 | +| OK(完整链路) | 24 | +| FIRE-AND-FORGET(仅日志/审计) | 25 | | PENDING(未实现) | 2 | +| erp-health 域事件 | 26 | +| erp-auth 域事件 | 11 | +| erp-config 域事件 | 8 | +| erp-workflow 域事件 | 6 | +| erp-plugin 域事件 | 2+(含动态) | ---