6c53eda71c
新增九类编辑器生成 durable receipt 与稳定提交指纹 统一资源、素材、binding、画布和队列终态的 SpacetimeDB 原子事务 补齐未知结果有界重放、CAS 刷新、lease fencing 与原子失败退款 收紧 owner、project、object、跨记录语义和 replay 一致性校验 保持 Agent 与 External compact 结果契约及 512 KiB 上限 延迟 inline 计费完成边界并区分 procedure dispatch 前后取消 同步 schema、迁移、生成绑定、架构文档和故障回归测试
223 lines
7.8 KiB
Rust
223 lines
7.8 KiB
Rust
use crate::*;
|
|
|
|
#[spacetimedb::table(
|
|
accessor = asset_entity_binding,
|
|
index(accessor = by_entity_slot, btree(columns = [entity_kind, entity_id, slot])),
|
|
index(accessor = by_asset_object_id, btree(columns = [asset_object_id]))
|
|
)]
|
|
pub struct AssetEntityBinding {
|
|
#[primary_key]
|
|
binding_id: String,
|
|
asset_object_id: String,
|
|
entity_kind: String,
|
|
entity_id: String,
|
|
slot: String,
|
|
asset_kind: String,
|
|
owner_user_id: Option<String>,
|
|
profile_id: Option<String>,
|
|
created_at: Timestamp,
|
|
updated_at: Timestamp,
|
|
}
|
|
|
|
// reducer 负责把已确认对象绑定到实体槽位,强业务资产表稳定前先用通用绑定表承接关系。
|
|
#[spacetimedb::reducer]
|
|
pub fn bind_asset_object_to_entity(
|
|
ctx: &ReducerContext,
|
|
input: AssetEntityBindingInput,
|
|
) -> Result<(), String> {
|
|
upsert_asset_entity_binding(ctx, input).map(|_| ())
|
|
}
|
|
|
|
// procedure 面向 Axum 同步绑定接口,返回最终绑定快照,避免 HTTP 层读取 private table。
|
|
#[spacetimedb::procedure]
|
|
pub fn bind_asset_object_to_entity_and_return(
|
|
ctx: &mut ProcedureContext,
|
|
input: AssetEntityBindingInput,
|
|
) -> AssetEntityBindingProcedureResult {
|
|
match ctx.try_with_tx(|tx| upsert_asset_entity_binding(tx, input.clone())) {
|
|
Ok(record) => AssetEntityBindingProcedureResult {
|
|
ok: true,
|
|
record: Some(record),
|
|
error_message: None,
|
|
},
|
|
Err(message) => AssetEntityBindingProcedureResult {
|
|
ok: false,
|
|
record: None,
|
|
error_message: Some(message),
|
|
},
|
|
}
|
|
}
|
|
|
|
pub(crate) fn upsert_asset_entity_binding(
|
|
ctx: &ReducerContext,
|
|
input: AssetEntityBindingInput,
|
|
) -> Result<AssetEntityBindingSnapshot, String> {
|
|
validate_asset_entity_binding_fields(
|
|
&input.binding_id,
|
|
&input.asset_object_id,
|
|
&input.entity_kind,
|
|
&input.entity_id,
|
|
&input.slot,
|
|
&input.asset_kind,
|
|
)
|
|
.map_err(|error| error.to_string())?;
|
|
|
|
if !has_asset_object(ctx, &input.asset_object_id) {
|
|
return Err("asset_entity_binding.asset_object_id 对应的 asset_object 不存在".to_string());
|
|
}
|
|
|
|
let current = ctx
|
|
.db
|
|
.asset_entity_binding()
|
|
.by_entity_slot()
|
|
.filter((
|
|
input.entity_kind.as_str(),
|
|
input.entity_id.as_str(),
|
|
input.slot.as_str(),
|
|
))
|
|
.next();
|
|
|
|
let snapshot = match current {
|
|
Some(existing) => {
|
|
ctx.db
|
|
.asset_entity_binding()
|
|
.binding_id()
|
|
.delete(&existing.binding_id);
|
|
let snapshot = AssetEntityBindingSnapshot {
|
|
binding_id: existing.binding_id.clone(),
|
|
asset_object_id: input.asset_object_id.clone(),
|
|
entity_kind: input.entity_kind.clone(),
|
|
entity_id: input.entity_id.clone(),
|
|
slot: input.slot.clone(),
|
|
asset_kind: input.asset_kind.clone(),
|
|
owner_user_id: input.owner_user_id.clone(),
|
|
profile_id: input.profile_id.clone(),
|
|
created_at_micros: existing.created_at.to_micros_since_unix_epoch(),
|
|
updated_at_micros: input.updated_at_micros,
|
|
};
|
|
ctx.db
|
|
.asset_entity_binding()
|
|
.insert(build_asset_entity_binding_row(&snapshot));
|
|
snapshot
|
|
}
|
|
None => {
|
|
let snapshot = AssetEntityBindingSnapshot {
|
|
binding_id: input.binding_id.clone(),
|
|
asset_object_id: input.asset_object_id.clone(),
|
|
entity_kind: input.entity_kind.clone(),
|
|
entity_id: input.entity_id.clone(),
|
|
slot: input.slot.clone(),
|
|
asset_kind: input.asset_kind.clone(),
|
|
owner_user_id: input.owner_user_id.clone(),
|
|
profile_id: input.profile_id.clone(),
|
|
created_at_micros: input.updated_at_micros,
|
|
updated_at_micros: input.updated_at_micros,
|
|
};
|
|
ctx.db
|
|
.asset_entity_binding()
|
|
.insert(build_asset_entity_binding_row(&snapshot));
|
|
snapshot
|
|
}
|
|
};
|
|
|
|
emit_asset_entity_binding_changed_event(ctx, &snapshot);
|
|
|
|
Ok(snapshot)
|
|
}
|
|
|
|
pub(crate) fn find_asset_entity_binding_snapshot_by_id(
|
|
ctx: &ReducerContext,
|
|
binding_id: &str,
|
|
) -> Option<AssetEntityBindingSnapshot> {
|
|
ctx.db
|
|
.asset_entity_binding()
|
|
.binding_id()
|
|
.find(&binding_id.trim().to_string())
|
|
.map(|row| AssetEntityBindingSnapshot {
|
|
binding_id: row.binding_id,
|
|
asset_object_id: row.asset_object_id,
|
|
entity_kind: row.entity_kind,
|
|
entity_id: row.entity_id,
|
|
slot: row.slot,
|
|
asset_kind: row.asset_kind,
|
|
owner_user_id: row.owner_user_id,
|
|
profile_id: row.profile_id,
|
|
created_at_micros: row.created_at.to_micros_since_unix_epoch(),
|
|
updated_at_micros: row.updated_at.to_micros_since_unix_epoch(),
|
|
})
|
|
}
|
|
|
|
pub(crate) fn find_asset_entity_binding_snapshot_by_slot(
|
|
ctx: &ReducerContext,
|
|
entity_kind: &str,
|
|
entity_id: &str,
|
|
slot: &str,
|
|
) -> Option<AssetEntityBindingSnapshot> {
|
|
ctx.db
|
|
.asset_entity_binding()
|
|
.by_entity_slot()
|
|
.filter((entity_kind.trim(), entity_id.trim(), slot.trim()))
|
|
.next()
|
|
.map(|row| AssetEntityBindingSnapshot {
|
|
binding_id: row.binding_id,
|
|
asset_object_id: row.asset_object_id,
|
|
entity_kind: row.entity_kind,
|
|
entity_id: row.entity_id,
|
|
slot: row.slot,
|
|
asset_kind: row.asset_kind,
|
|
owner_user_id: row.owner_user_id,
|
|
profile_id: row.profile_id,
|
|
created_at_micros: row.created_at.to_micros_since_unix_epoch(),
|
|
updated_at_micros: row.updated_at.to_micros_since_unix_epoch(),
|
|
})
|
|
}
|
|
|
|
fn build_asset_entity_binding_row(snapshot: &AssetEntityBindingSnapshot) -> AssetEntityBinding {
|
|
AssetEntityBinding {
|
|
binding_id: snapshot.binding_id.clone(),
|
|
asset_object_id: snapshot.asset_object_id.clone(),
|
|
entity_kind: snapshot.entity_kind.clone(),
|
|
entity_id: snapshot.entity_id.clone(),
|
|
slot: snapshot.slot.clone(),
|
|
asset_kind: snapshot.asset_kind.clone(),
|
|
owner_user_id: snapshot.owner_user_id.clone(),
|
|
profile_id: snapshot.profile_id.clone(),
|
|
created_at: Timestamp::from_micros_since_unix_epoch(snapshot.created_at_micros),
|
|
updated_at: Timestamp::from_micros_since_unix_epoch(snapshot.updated_at_micros),
|
|
}
|
|
}
|
|
|
|
fn emit_asset_entity_binding_changed_event(
|
|
ctx: &ReducerContext,
|
|
snapshot: &AssetEntityBindingSnapshot,
|
|
) {
|
|
let event = AssetEntityBindingChangedEvent {
|
|
binding_id: snapshot.binding_id.clone(),
|
|
asset_object_id: snapshot.asset_object_id.clone(),
|
|
entity_kind: snapshot.entity_kind.clone(),
|
|
entity_id: snapshot.entity_id.clone(),
|
|
slot: snapshot.slot.clone(),
|
|
asset_kind: snapshot.asset_kind.clone(),
|
|
owner_user_id: snapshot.owner_user_id.clone(),
|
|
profile_id: snapshot.profile_id.clone(),
|
|
occurred_at_micros: snapshot.updated_at_micros,
|
|
};
|
|
|
|
ctx.db.asset_event().insert(AssetEvent {
|
|
event_id: format!(
|
|
"assetevt_{}_{}_binding",
|
|
event.binding_id, event.occurred_at_micros
|
|
),
|
|
asset_object_id: event.asset_object_id,
|
|
binding_id: Some(event.binding_id),
|
|
event_kind: AssetEventKind::EntityBindingChanged,
|
|
asset_kind: event.asset_kind,
|
|
owner_user_id: event.owner_user_id,
|
|
profile_id: event.profile_id,
|
|
entity_kind: Some(event.entity_kind),
|
|
entity_id: Some(event.entity_id),
|
|
slot: Some(event.slot),
|
|
occurred_at: Timestamp::from_micros_since_unix_epoch(event.occurred_at_micros),
|
|
});
|
|
}
|