feat(plugin): P2-P4 插件平台演进 — 通用服务 + 质量保障 + 市场
P2 平台通用服务: - manifest 扩展: settings/numbering/templates/trigger_events/importable/exportable 声明 - 插件配置 UI: PluginSettingsForm 自动表单 + 后端校验 + 详情抽屉 Settings 标签页 - 编号规则: Host API numbering-generate + PostgreSQL 序列 + manifest 绑定 - 触发事件: data_service create/update/delete 自动发布 DomainEvent - WIT 接口: 新增 numbering-generate/setting-get Host API P3 质量保障: - plugin_validator.rs: 安全扫描(WASM大小/实体数量/字段校验) + 复杂度评分 - 运行时监控指标: RuntimeMetrics (错误率/响应时间/Fuel/内存) - 性能基准: BenchmarkResult 阈值定义 - 上传时自动安全扫描 + /validate API 端点 P4 插件市场: - 数据库迁移: plugin_market_entries + plugin_market_reviews 表 - 前端 PluginMarket 页面: 分类浏览/搜索/详情/评分 - 路由注册: /plugins/market 测试: 269 全通过 (71 erp-plugin + 41 auth + 57 config + 34 core + 50 message + 16 workflow)
This commit is contained in:
304
crates/erp-plugin/src/plugin_validator.rs
Normal file
304
crates/erp-plugin/src/plugin_validator.rs
Normal file
@@ -0,0 +1,304 @@
|
||||
use crate::error::{PluginError, PluginResult};
|
||||
use crate::manifest::{parse_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().map_or(false, |n| !n.is_empty());
|
||||
metrics.has_trigger_events = manifest.trigger_events.as_ref().map_or(false, |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::*;
|
||||
|
||||
#[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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user