Files
Genarrative/server-rs/crates/spacetime-module/src/runtime/feature_gate_config.rs
T
kdletters 25cac367c6 接入功能灰度与画布 Agent 灰度
新增 SpacetimeDB 灰度配置表与后端判定链路

新增后台灰度发布页面和两段式 Gate Key 选择器

将画布 Agent 入口接入灰度配置与运行时能力下发

修复灰度配置刷新读取空本地缓存的问题

补充灰度规则和后台下拉维护约定文档
2026-07-08 11:26:41 +08:00

105 lines
3.2 KiB
Rust

use crate::*;
#[spacetimedb::table(accessor = feature_gate_config)]
pub struct FeatureGateConfig {
#[primary_key]
pub(crate) gate_key: String,
pub(crate) enabled: bool,
pub(crate) rollout_percent: u32,
pub(crate) allow_user_ids: Vec<String>,
pub(crate) allow_user_tags: Vec<String>,
pub(crate) deny_user_ids: Vec<String>,
pub(crate) description: String,
pub(crate) updated_at: Timestamp,
}
#[spacetimedb::procedure]
pub fn get_feature_gate_config(ctx: &mut ProcedureContext) -> FeatureGateConfigProcedureResult {
match ctx.try_with_tx(|tx| Ok(list_feature_gate_config_snapshots(tx))) {
Ok(records) => FeatureGateConfigProcedureResult {
ok: true,
records,
error_message: None,
},
Err(message) => FeatureGateConfigProcedureResult {
ok: false,
records: vec![],
error_message: Some(message),
},
}
}
#[spacetimedb::procedure]
pub fn upsert_feature_gate_config(
ctx: &mut ProcedureContext,
input: FeatureGateConfigAdminUpsertInput,
) -> FeatureGateConfigProcedureResult {
match ctx.try_with_tx(|tx| upsert_feature_gate_config_in_tx(tx, input.clone())) {
Ok(records) => FeatureGateConfigProcedureResult {
ok: true,
records,
error_message: None,
},
Err(message) => FeatureGateConfigProcedureResult {
ok: false,
records: vec![],
error_message: Some(message),
},
}
}
fn upsert_feature_gate_config_in_tx(
ctx: &ReducerContext,
input: FeatureGateConfigAdminUpsertInput,
) -> Result<Vec<FeatureGateConfigSnapshot>, String> {
let input = module_runtime::normalize_feature_gate_admin_upsert_input(input)?;
let row = FeatureGateConfig {
gate_key: input.gate_key.clone(),
enabled: input.enabled,
rollout_percent: input.rollout_percent,
allow_user_ids: input.allow_user_ids,
allow_user_tags: input.allow_user_tags,
deny_user_ids: input.deny_user_ids,
description: input.description,
updated_at: ctx.timestamp,
};
if ctx
.db
.feature_gate_config()
.gate_key()
.find(&row.gate_key)
.is_some()
{
ctx.db.feature_gate_config().gate_key().update(row);
} else {
ctx.db.feature_gate_config().insert(row);
}
Ok(list_feature_gate_config_snapshots(ctx))
}
fn list_feature_gate_config_snapshots(ctx: &ReducerContext) -> Vec<FeatureGateConfigSnapshot> {
let mut records = ctx
.db
.feature_gate_config()
.iter()
.map(feature_gate_config_snapshot_from_row)
.collect::<Vec<_>>();
records.sort_by(|left, right| left.gate_key.cmp(&right.gate_key));
records
}
fn feature_gate_config_snapshot_from_row(row: FeatureGateConfig) -> FeatureGateConfigSnapshot {
FeatureGateConfigSnapshot {
gate_key: row.gate_key,
enabled: row.enabled,
rollout_percent: row.rollout_percent,
allow_user_ids: row.allow_user_ids,
allow_user_tags: row.allow_user_tags,
deny_user_ids: row.deny_user_ids,
description: row.description,
updated_at_micros: row.updated_at.to_micros_since_unix_epoch(),
}
}