306 lines
11 KiB
Rust
306 lines
11 KiB
Rust
#[spacetimedb::table(
|
|
accessor = asset_object,
|
|
index(accessor = by_bucket_object_key, btree(columns = [bucket, object_key]))
|
|
)]
|
|
pub struct AssetObject {
|
|
#[primary_key]
|
|
asset_object_id: String,
|
|
// 正式对象定位固定拆成 bucket + object_key 两列,避免后续再从单字符串路径做 schema 拆分。
|
|
bucket: String,
|
|
object_key: String,
|
|
access_policy: AssetObjectAccessPolicy,
|
|
content_type: Option<String>,
|
|
content_length: u64,
|
|
content_hash: Option<String>,
|
|
version: u32,
|
|
source_job_id: Option<String>,
|
|
owner_user_id: Option<String>,
|
|
profile_id: Option<String>,
|
|
entity_id: Option<String>,
|
|
#[index(btree)]
|
|
asset_kind: String,
|
|
created_at: Timestamp,
|
|
updated_at: Timestamp,
|
|
}
|
|
|
|
#[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 confirm_asset_object(
|
|
ctx: &ReducerContext,
|
|
input: AssetObjectUpsertInput,
|
|
) -> Result<(), String> {
|
|
upsert_asset_object(ctx, input).map(|_| ())
|
|
}
|
|
|
|
// procedure 面向 Axum 同步确认接口,返回最终持久化后的对象记录,避免 HTTP 层再额外查询 private table。
|
|
#[spacetimedb::procedure]
|
|
pub fn confirm_asset_object_and_return(
|
|
ctx: &mut ProcedureContext,
|
|
input: AssetObjectUpsertInput,
|
|
) -> AssetObjectProcedureResult {
|
|
match ctx.try_with_tx(|tx| upsert_asset_object(tx, input.clone())) {
|
|
Ok(record) => AssetObjectProcedureResult {
|
|
ok: true,
|
|
record: Some(record),
|
|
error_message: None,
|
|
},
|
|
Err(message) => AssetObjectProcedureResult {
|
|
ok: false,
|
|
record: None,
|
|
error_message: Some(message),
|
|
},
|
|
}
|
|
}
|
|
|
|
// 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),
|
|
},
|
|
}
|
|
}
|
|
fn upsert_asset_object(
|
|
ctx: &ReducerContext,
|
|
input: AssetObjectUpsertInput,
|
|
) -> Result<AssetObjectUpsertSnapshot, String> {
|
|
validate_asset_object_fields(
|
|
&input.bucket,
|
|
&input.object_key,
|
|
&input.asset_kind,
|
|
input.version,
|
|
)
|
|
.map_err(|error| error.to_string())?;
|
|
|
|
let updated_at = Timestamp::from_micros_since_unix_epoch(input.updated_at_micros);
|
|
// 这里先保持最小可发布实现:查重语义已经冻结,后续再把实现优化回组合索引扫描。
|
|
let current = ctx
|
|
.db
|
|
.asset_object()
|
|
.iter()
|
|
.find(|row| row.bucket == input.bucket && row.object_key == input.object_key);
|
|
|
|
let snapshot = match current {
|
|
Some(existing) => {
|
|
ctx.db
|
|
.asset_object()
|
|
.asset_object_id()
|
|
.delete(&existing.asset_object_id);
|
|
let row = AssetObject {
|
|
asset_object_id: existing.asset_object_id.clone(),
|
|
bucket: input.bucket.clone(),
|
|
object_key: input.object_key.clone(),
|
|
access_policy: input.access_policy,
|
|
content_type: input.content_type.clone(),
|
|
content_length: input.content_length,
|
|
content_hash: input.content_hash.clone(),
|
|
version: input.version,
|
|
source_job_id: input.source_job_id.clone(),
|
|
owner_user_id: input.owner_user_id.clone(),
|
|
profile_id: input.profile_id.clone(),
|
|
entity_id: input.entity_id.clone(),
|
|
asset_kind: input.asset_kind.clone(),
|
|
created_at: existing.created_at,
|
|
updated_at,
|
|
};
|
|
ctx.db.asset_object().insert(row);
|
|
|
|
AssetObjectUpsertSnapshot {
|
|
asset_object_id: existing.asset_object_id,
|
|
bucket: input.bucket,
|
|
object_key: input.object_key,
|
|
access_policy: input.access_policy,
|
|
content_type: input.content_type,
|
|
content_length: input.content_length,
|
|
content_hash: input.content_hash,
|
|
version: input.version,
|
|
source_job_id: input.source_job_id,
|
|
owner_user_id: input.owner_user_id,
|
|
profile_id: input.profile_id,
|
|
entity_id: input.entity_id,
|
|
asset_kind: input.asset_kind,
|
|
created_at_micros: existing.created_at.to_micros_since_unix_epoch(),
|
|
updated_at_micros: input.updated_at_micros,
|
|
}
|
|
}
|
|
None => {
|
|
let created_at = updated_at;
|
|
let row = AssetObject {
|
|
asset_object_id: input.asset_object_id.clone(),
|
|
bucket: input.bucket.clone(),
|
|
object_key: input.object_key.clone(),
|
|
access_policy: input.access_policy,
|
|
content_type: input.content_type.clone(),
|
|
content_length: input.content_length,
|
|
content_hash: input.content_hash.clone(),
|
|
version: input.version,
|
|
source_job_id: input.source_job_id.clone(),
|
|
owner_user_id: input.owner_user_id.clone(),
|
|
profile_id: input.profile_id.clone(),
|
|
entity_id: input.entity_id.clone(),
|
|
asset_kind: input.asset_kind.clone(),
|
|
created_at,
|
|
updated_at,
|
|
};
|
|
ctx.db.asset_object().insert(row);
|
|
|
|
AssetObjectUpsertSnapshot {
|
|
asset_object_id: input.asset_object_id,
|
|
bucket: input.bucket,
|
|
object_key: input.object_key,
|
|
access_policy: input.access_policy,
|
|
content_type: input.content_type,
|
|
content_length: input.content_length,
|
|
content_hash: input.content_hash,
|
|
version: input.version,
|
|
source_job_id: input.source_job_id,
|
|
owner_user_id: input.owner_user_id,
|
|
profile_id: input.profile_id,
|
|
entity_id: input.entity_id,
|
|
asset_kind: input.asset_kind,
|
|
created_at_micros: input.updated_at_micros,
|
|
updated_at_micros: input.updated_at_micros,
|
|
}
|
|
}
|
|
};
|
|
|
|
Ok(snapshot)
|
|
}
|
|
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 ctx
|
|
.db
|
|
.asset_object()
|
|
.asset_object_id()
|
|
.find(&input.asset_object_id)
|
|
.is_none()
|
|
{
|
|
return Err("asset_entity_binding.asset_object_id 对应的 asset_object 不存在".to_string());
|
|
}
|
|
|
|
let updated_at = Timestamp::from_micros_since_unix_epoch(input.updated_at_micros);
|
|
// 首版绑定按 entity_kind + entity_id + slot 幂等定位,后续访问量明确后再改为组合索引扫描。
|
|
let current = ctx.db.asset_entity_binding().iter().find(|row| {
|
|
row.entity_kind == input.entity_kind
|
|
&& row.entity_id == input.entity_id
|
|
&& row.slot == input.slot
|
|
});
|
|
|
|
let snapshot = match current {
|
|
Some(existing) => {
|
|
ctx.db
|
|
.asset_entity_binding()
|
|
.binding_id()
|
|
.delete(&existing.binding_id);
|
|
let row = AssetEntityBinding {
|
|
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: existing.created_at,
|
|
updated_at,
|
|
};
|
|
ctx.db.asset_entity_binding().insert(row);
|
|
|
|
AssetEntityBindingSnapshot {
|
|
binding_id: existing.binding_id,
|
|
asset_object_id: input.asset_object_id,
|
|
entity_kind: input.entity_kind,
|
|
entity_id: input.entity_id,
|
|
slot: input.slot,
|
|
asset_kind: input.asset_kind,
|
|
owner_user_id: input.owner_user_id,
|
|
profile_id: input.profile_id,
|
|
created_at_micros: existing.created_at.to_micros_since_unix_epoch(),
|
|
updated_at_micros: input.updated_at_micros,
|
|
}
|
|
}
|
|
None => {
|
|
let created_at = updated_at;
|
|
let row = AssetEntityBinding {
|
|
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,
|
|
updated_at,
|
|
};
|
|
ctx.db.asset_entity_binding().insert(row);
|
|
|
|
AssetEntityBindingSnapshot {
|
|
binding_id: input.binding_id,
|
|
asset_object_id: input.asset_object_id,
|
|
entity_kind: input.entity_kind,
|
|
entity_id: input.entity_id,
|
|
slot: input.slot,
|
|
asset_kind: input.asset_kind,
|
|
owner_user_id: input.owner_user_id,
|
|
profile_id: input.profile_id,
|
|
created_at_micros: input.updated_at_micros,
|
|
updated_at_micros: input.updated_at_micros,
|
|
}
|
|
}
|
|
};
|
|
|
|
Ok(snapshot)
|
|
}
|