feat(plugin): P1-P4 审计修复 — 第二批 (运行时监控 + 通知引擎 + 编号reset)

2.1 运行时监控:
- LoadedPlugin 新增 RuntimeMetrics (调用次数/错误/响应时间/燃料消耗)
- execute_wasm 自动采集每次调用的耗时和状态
- GET /admin/plugins/{id}/metrics 端点

2.2 通知规则引擎:
- notification.rs: 订阅 plugin.trigger.* 事件
- 触发时自动给管理员发送消息通知
- emit_trigger_events 增加 manifest_id 到 payload

2.3 编号 reset_rule:
- 替换 PostgreSQL SEQUENCE 为表行 + pg_advisory_xact_lock
- 支持 daily/monthly/yearly/never 重置周期
- 每个周期独立计数,切换时自动重置为 1
This commit is contained in:
iven
2026-04-19 14:41:17 +08:00
parent 4bcb4beaa5
commit 0a041c3d22
8 changed files with 283 additions and 28 deletions

View File

@@ -308,49 +308,101 @@ impl host_api::Host for HostState {
fn numbering_generate(&mut self, rule_key: String) -> Result<String, String> {
let rule = self.numbering_rules
.get(&rule_key)
.ok_or_else(|| format!("编号规则 '{}' 未声明", rule_key))?;
.ok_or_else(|| format!("编号规则 '{}' 未声明", rule_key))?
.clone();
let db = self.db.clone()
.ok_or("编号生成需要数据库连接")?;
// 使用 advisory lock 生成编号
let tenant_id = self.tenant_id;
let plugin_id = self.plugin_id.clone();
let rt = tokio::runtime::Handle::current();
rt.block_on(async {
// 简单实现:基于日期+序列
use sea_orm::{Statement, FromQueryResult, ConnectionTrait};
let now = chrono::Utc::now();
let year = now.format("%Y").to_string();
let month = now.format("%m").to_string();
let day = now.format("%d").to_string();
// 使用 PostgreSQL 序列确保并发安全
use sea_orm::{Statement, FromQueryResult};
#[derive(Debug, FromQueryResult)]
struct SeqVal { nextval: i64 }
// 计算当前周期的 key用于 reset_rule 判断)
let period_key = match rule.reset_rule.as_str() {
"daily" => format!("{}-{}-{}", year, month, day),
"monthly" => format!("{}-{}", year, month),
"yearly" => year.clone(),
_ => String::new(), // "never" — 不需要周期 key
};
let seq_name = format!("plugin_{}_{}_seq", self.plugin_id.replace('-', "_"), rule_key);
// 序列表名
let table_name = format!("plugin_numbering_seq_{}", plugin_id.replace('-', "_"));
// 确保序列表存在
let create_sql = format!(
"CREATE SEQUENCE IF NOT EXISTS {} START WITH 1 INCREMENT BY 1",
seq_name
"CREATE TABLE IF NOT EXISTS {} (\
rule_key VARCHAR(255) NOT NULL, \
period_key VARCHAR(64) NOT NULL DEFAULT '', \
current_val BIGINT NOT NULL DEFAULT 0, \
PRIMARY KEY (rule_key, period_key)\
)",
table_name
);
let result: Result<sea_orm::ExecResult, sea_orm::DbErr> = db.execute(Statement::from_string(
db.execute(Statement::from_string(
sea_orm::DatabaseBackend::Postgres,
create_sql,
)).await;
result.map_err(|e| format!("创建序列失败: {}", e))?;
)).await.map_err(|e| format!("创建序列表失败: {}", e))?;
let seq_sql = format!("SELECT nextval('{}') as nextval", seq_name);
let result: Option<SeqVal> = SeqVal::find_by_statement(Statement::from_string(
// 使用 advisory lock 保证并发安全
// lock_id 基于规则名哈希
let lock_id: i64 = {
use std::hash::{Hash, Hasher};
let mut hasher = std::collections::hash_map::DefaultHasher::new();
(plugin_id.clone() + &rule_key).hash(&mut hasher);
(hasher.finish() as i64).abs()
};
let lock_sql = format!("SELECT pg_advisory_xact_lock({})", lock_id);
db.execute(Statement::from_string(
sea_orm::DatabaseBackend::Postgres,
seq_sql,
)).one(&db).await.map_err(|e| format!("获取序列失败: {}", e))?;
lock_sql,
)).await.map_err(|e| format!("获取失败: {}", e))?;
let seq = result.map(|r| r.nextval).unwrap_or(1);
let seq_str = format!("{:0>width$}", seq, width = rule.seq_length as usize);
// 读取当前值
#[derive(Debug, FromQueryResult)]
struct SeqRow { current_val: i64 }
let read_sql = format!(
"SELECT current_val FROM {} WHERE rule_key = $1 AND period_key = $2",
table_name
);
let current = SeqRow::find_by_statement(Statement::from_sql_and_values(
sea_orm::DatabaseBackend::Postgres,
read_sql,
[rule_key.clone().into(), period_key.clone().into()],
)).one(&db).await.map_err(|e| format!("读取序列失败: {}", e))?;
let next_val = current.map(|r| r.current_val + 1).unwrap_or(1);
// UPSERT 新值
let upsert_sql = format!(
"INSERT INTO {} (rule_key, period_key, current_val) VALUES ($1, $2, $3) \
ON CONFLICT (rule_key, period_key) DO UPDATE SET current_val = $3",
table_name
);
db.execute(Statement::from_sql_and_values(
sea_orm::DatabaseBackend::Postgres,
upsert_sql,
[rule_key.clone().into(), period_key.clone().into(), next_val.into()],
)).await.map_err(|e| format!("更新序列失败: {}", e))?;
let seq_str = format!("{:0>width$}", next_val, width = rule.seq_length as usize);
let number = rule.format
.replace("{PREFIX}", &rule.prefix)
.replace("{YEAR}", &year)
.replace("{MONTH}", &month)
.replace("{DAY}", &day)
.replace(&format!("{{SEQ:{}}}", rule.seq_length), &seq_str)
.replace("{SEQ}", &seq_str);