feat: initialize Nuanji (Warm Notes) project
- Base platform from base.git (ERP base: auth, core, config, message, workflow, plugin) - Created erp-diary module skeleton (lib.rs, dto.rs, error.rs, event.rs, state.rs) - Integrated erp-diary into workspace and erp-server - Added DiaryModule registration in main.rs - Added DiaryState FromRef in state.rs - Diary routes mounted (empty routes, ready for implementation) - Product design spec v1.2 preserved in docs/ - Implementation plan preserved in plans/ Cargo check: OK Cargo test: OK (78+ base tests passing)
This commit is contained in:
317
crates/erp-plugin/src/plugin_validator.rs
Normal file
317
crates/erp-plugin/src/plugin_validator.rs
Normal file
@@ -0,0 +1,317 @@
|
||||
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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user