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)
305 lines
9.0 KiB
Rust
305 lines
9.0 KiB
Rust
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());
|
||
}
|
||
}
|