Files
hms/crates/erp-plugin/src/plugin_validator.rs
iven 6d5a711d2c
Some checks failed
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
CI / frontend-build (push) Has been cancelled
CI / security-audit (push) Has been cancelled
fix: 修复测试发现的 7 个问题 + 全 workspace clippy 清零
功能修复:
1. 患者创建空名称验证:后端添加 name.trim().is_empty() 检查
2. 仪表盘统计容错:单个查询失败返回零值而非 500
3. FHIR 路由修复:从 /fhir 移到 /api/v1/fhir 保持一致
4. 冻结模块后端中间件:新增 frozen_module_middleware 拦截冻结路径
5. 积分端点权限码:health.health-data.list → health.points.list
6. 角色权限迁移:护士补充 devices.list,运营补充 points.list/manage
7. 测试结果文档:R01-R05 角色测试 + T00/T10 结果归档

Clippy 全 workspace 清零(14→0 errors):
- erp-core: 修复 empty doc line、collapsible if、redundant closure 等 9 处
- erp-health: 修复 too_many_arguments、unused var、unnecessary parens 等 58 处
- erp-ai: 修复 dead_code、unused import 等 11 处
- erp-plugin: 修复 too_many_arguments、wildcard pattern 等 11 处
- erp-server-migration: 修复 enum_variant_names 5 处
- erp-auth/config/workflow/message: 各 1-3 处

工程改进:
- lint-staged 配置迁移到 .lintstagedrc.js(函数式避免文件列表传给 clippy)
- cargo fmt 统一格式化
2026-05-07 23:43:14 +08:00

318 lines
9.1 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use crate::error::PluginResult;
use crate::manifest::PluginManifest;
/// 插件上传时校验报告
#[derive(Debug, Clone, serde::Serialize)]
pub struct ValidationReport {
pub valid: bool,
pub errors: Vec<String>,
pub warnings: Vec<String>,
pub metrics: PluginMetrics,
}
/// 插件质量指标
#[derive(Debug, Clone, Default, serde::Serialize)]
pub struct PluginMetrics {
pub entity_count: usize,
pub field_count: usize,
pub page_count: usize,
pub permission_count: usize,
pub relation_count: usize,
pub has_import_export: bool,
pub has_settings: bool,
pub has_numbering: bool,
pub has_trigger_events: bool,
pub wasm_size_bytes: usize,
pub complexity_score: f64,
}
/// 运行时监控指标
#[derive(Debug, Clone, Default, serde::Serialize)]
pub struct RuntimeMetrics {
pub error_count: u64,
pub total_invocations: u64,
pub avg_response_ms: f64,
pub fuel_consumption_avg: f64,
pub memory_peak_bytes: u64,
pub last_error: Option<String>,
pub last_error_at: Option<chrono::DateTime<chrono::Utc>>,
}
impl RuntimeMetrics {
pub fn error_rate(&self) -> f64 {
if self.total_invocations == 0 {
return 0.0;
}
self.error_count as f64 / self.total_invocations as f64
}
}
/// 上传时安全扫描
pub fn validate_plugin_security(
manifest: &PluginManifest,
wasm_size: usize,
) -> PluginResult<ValidationReport> {
let mut errors = Vec::new();
let mut warnings = Vec::new();
// 1. WASM 大小检查(上限 10MB
if wasm_size > 10 * 1024 * 1024 {
errors.push(format!("WASM 文件过大: {} bytes (上限 10MB)", wasm_size));
} else if wasm_size > 5 * 1024 * 1024 {
warnings.push(format!("WASM 文件较大: {} bytes (>5MB)", wasm_size));
}
// 2. 实体数量检查(上限 20
if let Some(schema) = &manifest.schema {
if schema.entities.len() > 20 {
errors.push(format!("实体数量过多: {} (上限 20)", schema.entities.len()));
}
for entity in &schema.entities {
// 字段数量检查
if entity.fields.len() > 50 {
errors.push(format!(
"实体 '{}' 字段数量过多: {} (上限 50)",
entity.name,
entity.fields.len()
));
}
// 索引数量检查
if entity.indexes.len() > 10 {
warnings.push(format!(
"实体 '{}' 索引数量较多: {} (>10 可能影响写入性能)",
entity.name,
entity.indexes.len()
));
}
// 检查字段中有无潜在 SQL 注入风险的字段名
for field in &entity.fields {
if field.name.len() > 64 {
errors.push(format!(
"字段名过长: '{}.{}' (上限 64 字符)",
entity.name, field.name
));
}
if !field
.name
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '_')
{
errors.push(format!(
"字段名包含非法字符: '{}.{}' (只允许字母、数字、下划线)",
entity.name, field.name
));
}
}
}
}
// 3. 权限码命名规范检查
if let Some(permissions) = &manifest.permissions {
for perm in permissions {
if !perm.code.contains('.') {
warnings.push(format!(
"权限码 '{}' 建议使用 'entity.action' 格式",
perm.code
));
}
}
}
// 4. 依赖检查
if manifest.metadata.dependencies.len() > 5 {
warnings.push(format!(
"依赖数量较多: {} (>5 可能增加安装复杂度)",
manifest.metadata.dependencies.len()
));
}
// 5. 计算复杂度分数
let mut metrics = collect_metrics(manifest, wasm_size);
metrics.complexity_score = calculate_complexity_score(&metrics);
if metrics.complexity_score > 80.0 {
warnings.push(format!(
"插件复杂度较高: {:.1} (>80 建议拆分)",
metrics.complexity_score
));
}
let valid = errors.is_empty();
Ok(ValidationReport {
valid,
errors,
warnings,
metrics,
})
}
/// 收集插件指标
fn collect_metrics(manifest: &PluginManifest, wasm_size: usize) -> PluginMetrics {
let mut metrics = PluginMetrics {
wasm_size_bytes: wasm_size,
..Default::default()
};
if let Some(schema) = &manifest.schema {
metrics.entity_count = schema.entities.len();
for entity in &schema.entities {
metrics.field_count += entity.fields.len();
metrics.relation_count += entity.relations.len();
if entity.importable == Some(true) || entity.exportable == Some(true) {
metrics.has_import_export = true;
}
}
}
if let Some(ui) = &manifest.ui {
metrics.page_count = count_pages(&ui.pages);
}
if let Some(permissions) = &manifest.permissions {
metrics.permission_count = permissions.len();
}
metrics.has_settings = manifest.settings.is_some();
metrics.has_numbering = manifest.numbering.as_ref().is_some_and(|n| !n.is_empty());
metrics.has_trigger_events = manifest
.trigger_events
.as_ref()
.is_some_and(|t| !t.is_empty());
metrics
}
fn count_pages(pages: &[crate::manifest::PluginPageType]) -> usize {
let mut count = 0;
for page in pages {
count += 1;
if let crate::manifest::PluginPageType::Tabs { tabs, .. } = page {
count += count_pages(tabs);
}
}
count
}
/// 计算复杂度分数0-100
fn calculate_complexity_score(metrics: &PluginMetrics) -> f64 {
let entity_score = (metrics.entity_count as f64 / 20.0) * 30.0;
let field_score = (metrics.field_count as f64 / 100.0) * 20.0;
let page_score = (metrics.page_count as f64 / 20.0) * 15.0;
let relation_score = (metrics.relation_count as f64 / 30.0) * 15.0;
let size_score = (metrics.wasm_size_bytes as f64 / (10.0 * 1024.0 * 1024.0)) * 20.0;
(entity_score + field_score + page_score + relation_score + size_score).min(100.0)
}
/// 性能基准测试结果
#[derive(Debug, Clone, serde::Serialize)]
pub struct BenchmarkResult {
pub create_avg_ms: f64,
pub read_avg_ms: f64,
pub update_avg_ms: f64,
pub delete_avg_ms: f64,
pub list_avg_ms: f64,
pub passed: bool,
pub details: String,
}
impl BenchmarkResult {
/// 创建操作的阈值: 500ms
pub const CREATE_THRESHOLD_MS: f64 = 500.0;
/// 读取操作的阈值: 200ms
pub const READ_THRESHOLD_MS: f64 = 200.0;
/// 列表查询的阈值: 1000ms
pub const LIST_THRESHOLD_MS: f64 = 1000.0;
pub fn check(&self) -> bool {
self.create_avg_ms <= Self::CREATE_THRESHOLD_MS
&& self.read_avg_ms <= Self::READ_THRESHOLD_MS
&& self.list_avg_ms <= Self::LIST_THRESHOLD_MS
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::manifest::parse_manifest;
#[test]
fn validate_security_basic() {
let toml = r#"
[metadata]
id = "test"
name = "Test"
version = "0.1.0"
[schema]
[[schema.entities]]
name = "product"
display_name = "商品"
[[schema.entities.fields]]
name = "sku"
field_type = "string"
required = true
"#;
let manifest = parse_manifest(toml).unwrap();
let report = validate_plugin_security(&manifest, 1024).unwrap();
assert!(report.valid);
assert!(report.errors.is_empty());
}
#[test]
fn reject_oversized_wasm() {
let toml = r#"
[metadata]
id = "test"
name = "Test"
version = "0.1.0"
"#;
let manifest = parse_manifest(toml).unwrap();
let report = validate_plugin_security(&manifest, 15 * 1024 * 1024).unwrap();
assert!(!report.valid);
assert!(report.errors.iter().any(|e| e.contains("WASM 文件过大")));
}
#[test]
fn complexity_score_calculation() {
let metrics = PluginMetrics {
entity_count: 5,
field_count: 30,
page_count: 5,
relation_count: 3,
wasm_size_bytes: 500_000,
..Default::default()
};
let score = calculate_complexity_score(&metrics);
assert!(score > 0.0 && score < 50.0, "score = {}", score);
}
#[test]
fn runtime_metrics_error_rate() {
let metrics = RuntimeMetrics {
error_count: 5,
total_invocations: 100,
..Default::default()
};
assert!((metrics.error_rate() - 0.05).abs() < 0.001);
}
#[test]
fn benchmark_threshold_check() {
let result = BenchmarkResult {
create_avg_ms: 300.0,
read_avg_ms: 100.0,
update_avg_ms: 200.0,
delete_avg_ms: 150.0,
list_avg_ms: 800.0,
passed: true,
details: String::new(),
};
assert!(result.check());
}
}