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:
438
crates/erp-plugin/src/host.rs
Normal file
438
crates/erp-plugin/src/host.rs
Normal file
@@ -0,0 +1,438 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use sea_orm::DatabaseConnection;
|
||||
use uuid::Uuid;
|
||||
use wasmtime::StoreLimits;
|
||||
|
||||
use crate::dynamic_table::DynamicTableManager;
|
||||
use crate::engine::PluginEngine;
|
||||
use crate::erp::plugin::host_api;
|
||||
|
||||
/// 待刷新的写操作
|
||||
#[derive(Debug)]
|
||||
pub enum PendingOp {
|
||||
Insert {
|
||||
id: String,
|
||||
entity: String,
|
||||
data: Vec<u8>,
|
||||
},
|
||||
Update {
|
||||
entity: String,
|
||||
id: String,
|
||||
data: Vec<u8>,
|
||||
version: i64,
|
||||
},
|
||||
Delete {
|
||||
entity: String,
|
||||
id: String,
|
||||
},
|
||||
PublishEvent {
|
||||
event_type: String,
|
||||
payload: Vec<u8>,
|
||||
},
|
||||
}
|
||||
|
||||
/// Host 端状态 — 绑定到每个 WASM Store 实例
|
||||
///
|
||||
/// 支持两种执行模式:
|
||||
/// - **预填充模式**(db = None):读操作从预填充缓存取,向后兼容
|
||||
/// - **混合执行模式**(db = Some):读操作走实时 SQL + 写操作保持延迟批量
|
||||
pub struct HostState {
|
||||
pub(crate) limits: StoreLimits,
|
||||
#[allow(dead_code)]
|
||||
pub(crate) tenant_id: Uuid,
|
||||
#[allow(dead_code)]
|
||||
pub(crate) user_id: Uuid,
|
||||
pub(crate) permissions: Vec<String>,
|
||||
pub(crate) plugin_id: String,
|
||||
// 预填充的读取缓存(向后兼容)
|
||||
pub(crate) query_results: HashMap<String, Vec<u8>>,
|
||||
pub(crate) config_cache: HashMap<String, Vec<u8>>,
|
||||
pub(crate) current_user_json: Vec<u8>,
|
||||
// 待刷新的写操作
|
||||
pub(crate) pending_ops: Vec<PendingOp>,
|
||||
// 日志
|
||||
pub(crate) logs: Vec<(String, String)>,
|
||||
// 混合执行模式:数据库连接和事件总线
|
||||
pub(crate) db: Option<DatabaseConnection>,
|
||||
pub(crate) event_bus: Option<erp_core::events::EventBus>,
|
||||
// 跨插件实体映射:"erp-crm.customer" → "plugin_erp_crm__customer"
|
||||
pub(crate) cross_plugin_entities: HashMap<String, String>,
|
||||
// 编号规则映射:"invoice" → "INV-{YEAR}-{SEQ:4}"
|
||||
pub(crate) numbering_rules: HashMap<String, NumberingRule>,
|
||||
// 插件配置值
|
||||
pub(crate) plugin_config: serde_json::Value,
|
||||
}
|
||||
|
||||
/// 编号规则缓存
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NumberingRule {
|
||||
pub prefix: String,
|
||||
pub format: String,
|
||||
pub seq_length: u32,
|
||||
pub reset_rule: String,
|
||||
}
|
||||
|
||||
impl HostState {
|
||||
pub fn new(
|
||||
plugin_id: String,
|
||||
tenant_id: Uuid,
|
||||
user_id: Uuid,
|
||||
permissions: Vec<String>,
|
||||
) -> Self {
|
||||
let current_user = serde_json::json!({
|
||||
"id": user_id.to_string(),
|
||||
"tenant_id": tenant_id.to_string(),
|
||||
});
|
||||
Self {
|
||||
limits: wasmtime::StoreLimitsBuilder::new().build(),
|
||||
tenant_id,
|
||||
user_id,
|
||||
permissions,
|
||||
plugin_id,
|
||||
query_results: HashMap::new(),
|
||||
config_cache: HashMap::new(),
|
||||
current_user_json: serde_json::to_vec(¤t_user).unwrap_or_default(),
|
||||
pending_ops: Vec::new(),
|
||||
logs: Vec::new(),
|
||||
db: None,
|
||||
event_bus: None,
|
||||
cross_plugin_entities: HashMap::new(),
|
||||
numbering_rules: HashMap::new(),
|
||||
plugin_config: serde_json::json!({}),
|
||||
}
|
||||
}
|
||||
|
||||
/// 创建带数据库连接的 HostState(混合执行模式)
|
||||
pub fn new_with_db(
|
||||
plugin_id: String,
|
||||
tenant_id: Uuid,
|
||||
user_id: Uuid,
|
||||
permissions: Vec<String>,
|
||||
db: DatabaseConnection,
|
||||
event_bus: erp_core::events::EventBus,
|
||||
) -> Self {
|
||||
let mut state = Self::new(plugin_id, tenant_id, user_id, permissions);
|
||||
state.db = Some(db);
|
||||
state.event_bus = Some(event_bus);
|
||||
state
|
||||
}
|
||||
}
|
||||
|
||||
// 实现 bindgen 生成的 Host trait — 插件调用 Host API 的入口
|
||||
impl host_api::Host for HostState {
|
||||
fn db_insert(&mut self, entity: String, data: Vec<u8>) -> Result<Vec<u8>, String> {
|
||||
let id = Uuid::now_v7().to_string();
|
||||
let response = serde_json::json!({
|
||||
"id": id,
|
||||
"entity": entity,
|
||||
"status": "queued",
|
||||
});
|
||||
self.pending_ops.push(PendingOp::Insert {
|
||||
id: id.clone(),
|
||||
entity,
|
||||
data,
|
||||
});
|
||||
serde_json::to_vec(&response).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
fn db_query(
|
||||
&mut self,
|
||||
entity: String,
|
||||
filter: Vec<u8>,
|
||||
pagination: Vec<u8>,
|
||||
) -> Result<Vec<u8>, String> {
|
||||
// 预填充模式(向后兼容)
|
||||
if self.db.is_none() {
|
||||
return self
|
||||
.query_results
|
||||
.get(&entity)
|
||||
.cloned()
|
||||
.ok_or_else(|| format!("实体 '{}' 的查询结果未预填充", entity));
|
||||
}
|
||||
|
||||
let db = self.db.clone().ok_or("数据库连接不可用")?;
|
||||
let event_bus = self.event_bus.clone().ok_or("事件总线不可用")?;
|
||||
|
||||
// 先 flush pending writes(确保读后写一致性)
|
||||
let ops = std::mem::take(&mut self.pending_ops);
|
||||
if !ops.is_empty() {
|
||||
let rt = tokio::runtime::Handle::current();
|
||||
rt.block_on(PluginEngine::flush_ops(
|
||||
&db,
|
||||
&self.plugin_id,
|
||||
ops,
|
||||
self.tenant_id,
|
||||
self.user_id,
|
||||
&event_bus,
|
||||
))
|
||||
.map_err(|e| format!("flush pending ops 失败: {}", e))?;
|
||||
}
|
||||
|
||||
// 解析 filter 和 pagination
|
||||
let filter_val: Option<serde_json::Value> = if filter.is_empty() {
|
||||
None
|
||||
} else {
|
||||
serde_json::from_slice(&filter).ok()
|
||||
};
|
||||
|
||||
let pagination_val: Option<serde_json::Value> = if pagination.is_empty() {
|
||||
None
|
||||
} else {
|
||||
serde_json::from_slice(&pagination).ok()
|
||||
};
|
||||
|
||||
// 构建查询 — 支持点分记号跨插件查询(如 "erp-crm.customer")
|
||||
let table_name = if entity.contains('.') {
|
||||
self.cross_plugin_entities
|
||||
.get(&entity)
|
||||
.cloned()
|
||||
.ok_or_else(|| format!("跨插件实体 '{}' 未注册", entity))?
|
||||
} else {
|
||||
DynamicTableManager::table_name(&self.plugin_id, &entity)
|
||||
};
|
||||
|
||||
let limit = pagination_val
|
||||
.as_ref()
|
||||
.and_then(|p| p.get("limit"))
|
||||
.and_then(|v| v.as_u64())
|
||||
.unwrap_or(50);
|
||||
let offset = pagination_val
|
||||
.as_ref()
|
||||
.and_then(|p| p.get("offset"))
|
||||
.and_then(|v| v.as_u64())
|
||||
.unwrap_or(0);
|
||||
|
||||
let (sql, values) = DynamicTableManager::build_filtered_query_sql(
|
||||
&table_name,
|
||||
self.tenant_id,
|
||||
limit,
|
||||
offset,
|
||||
filter_val,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.map_err(|e| format!("查询构建失败: {}", e))?;
|
||||
|
||||
// 执行查询
|
||||
let rt = tokio::runtime::Handle::current();
|
||||
let rows = rt
|
||||
.block_on(async {
|
||||
use sea_orm::{FromQueryResult, Statement};
|
||||
#[derive(Debug, FromQueryResult)]
|
||||
struct QueryRow {
|
||||
data: serde_json::Value,
|
||||
}
|
||||
|
||||
let results = QueryRow::find_by_statement(Statement::from_sql_and_values(
|
||||
sea_orm::DatabaseBackend::Postgres,
|
||||
sql,
|
||||
values,
|
||||
))
|
||||
.all(&db)
|
||||
.await
|
||||
.map_err(|e| format!("查询执行失败: {}", e))?;
|
||||
|
||||
let items: Vec<serde_json::Value> = results.into_iter().map(|r| r.data).collect();
|
||||
|
||||
Ok::<Vec<serde_json::Value>, String>(items)
|
||||
})
|
||||
.map_err(|e: String| e)?;
|
||||
|
||||
serde_json::to_vec(&rows).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
fn db_update(
|
||||
&mut self,
|
||||
entity: String,
|
||||
id: String,
|
||||
data: Vec<u8>,
|
||||
version: i64,
|
||||
) -> Result<Vec<u8>, String> {
|
||||
let response = serde_json::json!({
|
||||
"id": id,
|
||||
"entity": entity,
|
||||
"version": version + 1,
|
||||
"status": "queued",
|
||||
});
|
||||
self.pending_ops.push(PendingOp::Update {
|
||||
entity,
|
||||
id,
|
||||
data,
|
||||
version,
|
||||
});
|
||||
serde_json::to_vec(&response).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
fn db_delete(&mut self, entity: String, id: String) -> Result<(), String> {
|
||||
self.pending_ops.push(PendingOp::Delete { entity, id });
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn event_publish(&mut self, event_type: String, payload: Vec<u8>) -> Result<(), String> {
|
||||
self.pending_ops.push(PendingOp::PublishEvent {
|
||||
event_type,
|
||||
payload,
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn config_get(&mut self, key: String) -> Result<Vec<u8>, String> {
|
||||
self.config_cache
|
||||
.get(&key)
|
||||
.cloned()
|
||||
.ok_or_else(|| format!("配置项 '{}' 未预填充", key))
|
||||
}
|
||||
|
||||
fn log_write(&mut self, level: String, message: String) {
|
||||
tracing::info!(
|
||||
plugin = %self.plugin_id,
|
||||
level = %level,
|
||||
"Plugin log: {}",
|
||||
message
|
||||
);
|
||||
self.logs.push((level, message));
|
||||
}
|
||||
|
||||
fn current_user(&mut self) -> Result<Vec<u8>, String> {
|
||||
Ok(self.current_user_json.clone())
|
||||
}
|
||||
|
||||
fn check_permission(&mut self, permission: String) -> Result<bool, String> {
|
||||
Ok(self.permissions.contains(&permission))
|
||||
}
|
||||
|
||||
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))?
|
||||
.clone();
|
||||
|
||||
let db = self.db.clone().ok_or("编号生成需要数据库连接")?;
|
||||
|
||||
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::{ConnectionTrait, FromQueryResult, Statement};
|
||||
|
||||
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();
|
||||
|
||||
// 计算当前周期的 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
|
||||
};
|
||||
|
||||
// 序列表名(使用 sanitize_identifier 防注入)
|
||||
let table_name = format!(
|
||||
"plugin_numbering_seq_{}",
|
||||
crate::dynamic_table::sanitize_identifier(&plugin_id)
|
||||
);
|
||||
|
||||
// 确保序列表存在
|
||||
let create_sql = format!(
|
||||
"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
|
||||
);
|
||||
db.execute(Statement::from_string(
|
||||
sea_orm::DatabaseBackend::Postgres,
|
||||
create_sql,
|
||||
))
|
||||
.await
|
||||
.map_err(|e| format!("创建序列表失败: {}", e))?;
|
||||
|
||||
// 使用 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,
|
||||
lock_sql,
|
||||
))
|
||||
.await
|
||||
.map_err(|e| format!("获取锁失败: {}", e))?;
|
||||
|
||||
// 读取当前值
|
||||
#[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);
|
||||
|
||||
Ok(number)
|
||||
})
|
||||
}
|
||||
|
||||
fn setting_get(&mut self, key: String) -> Result<Vec<u8>, String> {
|
||||
let config = self
|
||||
.plugin_config
|
||||
.as_object()
|
||||
.ok_or("插件配置不是有效对象")?;
|
||||
let value = config.get(&key).cloned().unwrap_or(serde_json::Value::Null);
|
||||
serde_json::to_vec(&value).map_err(|e| e.to_string())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user