fix(admin): decode recharge order enum cells
This commit is contained in:
@@ -662,6 +662,14 @@
|
|||||||
- 验证:mock 通知测试只能覆盖本地回调推进;真实环境还需用微信支付平台公钥、真实通知头和 API v3 密钥验证签名与解密链路。
|
- 验证:mock 通知测试只能覆盖本地回调推进;真实环境还需用微信支付平台公钥、真实通知头和 API v3 密钥验证签名与解密链路。
|
||||||
- 关联:`server-rs/crates/api-server/src/wechat_pay.rs`、`docs/technical/MY_TAB_ACCOUNT_RECHARGE_IMPLEMENTATION_2026-04-25.md`。
|
- 关联:`server-rs/crates/api-server/src/wechat_pay.rs`、`docs/technical/MY_TAB_ACCOUNT_RECHARGE_IMPLEMENTATION_2026-04-25.md`。
|
||||||
|
|
||||||
|
## 后台表查询展示 SpacetimeDB 枚举时不要套用 Option 解码
|
||||||
|
|
||||||
|
- 现象:后台“表查询”查看 `profile_recharge_order` 时,`kind` 和 `status` 显示为空数组 `[]`,例如充值订单原始行里 `points_60` 的类型和状态都不可读。
|
||||||
|
- 原因:SpacetimeDB HTTP SQL 对无载荷枚举会返回 SATS 形态 `[variant_index, []]`;后台通用 normalizer 曾把任何 `[0, value]` 都当作 `Option::Some(value)` 展开,导致 `[0, []]` 最终只剩 `[]`。
|
||||||
|
- 处理:通用表查询解析应先按表名和列名识别已知业务枚举,再落回 Option / Timestamp 通用展开;例如 `profile_recharge_order.kind` 映射为 `points` / `membership`,`profile_recharge_order.status` 映射为 `pending` / `paid` / `failed` / `closed` / `refunded`。
|
||||||
|
- 验证:执行 `cargo test -p api-server admin_database -- --nocapture`,并确认后台详情弹层的 `raw` 与表格 `cells` 都显示业务字符串。
|
||||||
|
- 关联:`server-rs/crates/api-server/src/admin.rs`、`docs/technical/ADMIN_DATABASE_TABLE_QUERY_2026-05-08.md`。
|
||||||
|
|
||||||
## 抓大鹅历史草稿外部 Rodin GLB 链接必须转存后再试玩或发布
|
## 抓大鹅历史草稿外部 Rodin GLB 链接必须转存后再试玩或发布
|
||||||
|
|
||||||
- 现象:草稿页预览模型失败并报 `GL_INVALID_ENUM: Invalid cap.`,或结果页能看到历史生成记录但试玩、发布和正式运行态仍显示默认积木。
|
- 现象:草稿页预览模型失败并报 `GL_INVALID_ENUM: Invalid cap.`,或结果页能看到历史生成记录但试玩、发布和正式运行态仍显示默认积木。
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ Query:
|
|||||||
- SQL 固定为 `SELECT * FROM {tableName} LIMIT {limit}`;SpacetimeDB 2.2 HTTP SQL 不拼 `ORDER BY`。
|
- SQL 固定为 `SELECT * FROM {tableName} LIMIT {limit}`;SpacetimeDB 2.2 HTTP SQL 不拼 `ORDER BY`。
|
||||||
- 用户输入不直接拼入 SQL;关键词和条件在 API Server 内存中过滤。
|
- 用户输入不直接拼入 SQL;关键词和条件在 API Server 内存中过滤。
|
||||||
- private 表或 token 不可见时返回后台可读错误信息。
|
- private 表或 token 不可见时返回后台可读错误信息。
|
||||||
- SpacetimeDB SQL 行和 SATS 值统一转成人可读 JSON:Option None 为 null,Some 展开为内部值,Timestamp 单元素数组展开为内部值,enum 可保留 tag/name 或原始数组文本。
|
- SpacetimeDB SQL 行和 SATS 值统一转成人可读 JSON:Option None 为 null,Some 展开为内部值,Timestamp 单元素数组展开为内部值;已知业务枚举列应在 API Server 按表名和列名转换为业务字符串,例如 `profile_recharge_order.kind` 转为 `points` / `membership`,`profile_recharge_order.status` 转为 `pending` / `paid` / `failed` / `closed` / `refunded`。
|
||||||
|
|
||||||
## 前端页面
|
## 前端页面
|
||||||
|
|
||||||
|
|||||||
@@ -725,7 +725,7 @@ fn parse_admin_database_table_rows_sql_response(
|
|||||||
.ok_or_else(|| "SQL rows 字段格式非法".to_string())?;
|
.ok_or_else(|| "SQL rows 字段格式非法".to_string())?;
|
||||||
let rows = row_values
|
let rows = row_values
|
||||||
.iter()
|
.iter()
|
||||||
.map(|row| build_admin_database_table_row(row, &columns))
|
.map(|row| build_admin_database_table_row_for_table(table_name, row, &columns))
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
Ok(AdminDatabaseTableRowsResponse {
|
Ok(AdminDatabaseTableRowsResponse {
|
||||||
table_name: table_name.to_string(),
|
table_name: table_name.to_string(),
|
||||||
@@ -769,7 +769,15 @@ fn extract_sql_statement_columns(statement: &Value) -> Vec<String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn build_admin_database_table_row(row: &Value, columns: &[String]) -> AdminDatabaseTableRowPayload {
|
fn build_admin_database_table_row(row: &Value, columns: &[String]) -> AdminDatabaseTableRowPayload {
|
||||||
let raw = normalize_admin_database_value(row);
|
build_admin_database_table_row_for_table("", row, columns)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_admin_database_table_row_for_table(
|
||||||
|
table_name: &str,
|
||||||
|
row: &Value,
|
||||||
|
columns: &[String],
|
||||||
|
) -> AdminDatabaseTableRowPayload {
|
||||||
|
let raw = normalize_admin_database_table_row_raw(table_name, row, columns);
|
||||||
let mut cells = Map::new();
|
let mut cells = Map::new();
|
||||||
if let Some(values) = row.as_array() {
|
if let Some(values) = row.as_array() {
|
||||||
for (index, value) in values.iter().enumerate() {
|
for (index, value) in values.iter().enumerate() {
|
||||||
@@ -777,11 +785,17 @@ fn build_admin_database_table_row(row: &Value, columns: &[String]) -> AdminDatab
|
|||||||
.get(index)
|
.get(index)
|
||||||
.cloned()
|
.cloned()
|
||||||
.unwrap_or_else(|| format!("col_{}", index + 1));
|
.unwrap_or_else(|| format!("col_{}", index + 1));
|
||||||
cells.insert(key, normalize_admin_database_value(value));
|
cells.insert(
|
||||||
|
key.clone(),
|
||||||
|
normalize_admin_database_table_cell(table_name, &key, value),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else if let Some(object) = row.as_object() {
|
} else if let Some(object) = row.as_object() {
|
||||||
for (key, value) in object {
|
for (key, value) in object {
|
||||||
cells.insert(key.clone(), normalize_admin_database_value(value));
|
cells.insert(
|
||||||
|
key.clone(),
|
||||||
|
normalize_admin_database_table_cell(table_name, key, value),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
AdminDatabaseTableRowPayload {
|
AdminDatabaseTableRowPayload {
|
||||||
@@ -790,6 +804,85 @@ fn build_admin_database_table_row(row: &Value, columns: &[String]) -> AdminDatab
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn normalize_admin_database_table_row_raw(
|
||||||
|
table_name: &str,
|
||||||
|
row: &Value,
|
||||||
|
columns: &[String],
|
||||||
|
) -> Value {
|
||||||
|
if let Some(values) = row.as_array() {
|
||||||
|
return Value::Array(
|
||||||
|
values
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(index, value)| {
|
||||||
|
let key = columns.get(index).map(String::as_str).unwrap_or_default();
|
||||||
|
normalize_admin_database_table_cell(table_name, key, value)
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(object) = row.as_object() {
|
||||||
|
return Value::Object(
|
||||||
|
object
|
||||||
|
.iter()
|
||||||
|
.map(|(key, value)| {
|
||||||
|
(
|
||||||
|
key.clone(),
|
||||||
|
normalize_admin_database_table_cell(table_name, key, value),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
normalize_admin_database_value(row)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalize_admin_database_table_cell(
|
||||||
|
table_name: &str,
|
||||||
|
column_name: &str,
|
||||||
|
value: &Value,
|
||||||
|
) -> Value {
|
||||||
|
if let Some(enum_value) = normalize_admin_database_known_enum(table_name, column_name, value) {
|
||||||
|
return enum_value;
|
||||||
|
}
|
||||||
|
normalize_admin_database_value(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalize_admin_database_known_enum(
|
||||||
|
table_name: &str,
|
||||||
|
column_name: &str,
|
||||||
|
value: &Value,
|
||||||
|
) -> Option<Value> {
|
||||||
|
let variant_index = extract_sats_enum_variant_index(value)?;
|
||||||
|
let label = match (table_name, column_name) {
|
||||||
|
("profile_recharge_order", "kind") => match variant_index {
|
||||||
|
0 => "points",
|
||||||
|
1 => "membership",
|
||||||
|
_ => return None,
|
||||||
|
},
|
||||||
|
("profile_recharge_order", "status") => match variant_index {
|
||||||
|
0 => "pending",
|
||||||
|
1 => "paid",
|
||||||
|
2 => "failed",
|
||||||
|
3 => "closed",
|
||||||
|
4 => "refunded",
|
||||||
|
_ => return None,
|
||||||
|
},
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
Some(Value::String(label.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_sats_enum_variant_index(value: &Value) -> Option<u64> {
|
||||||
|
let items = value.as_array()?;
|
||||||
|
if items.len() != 2 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
items.first()?.as_u64()
|
||||||
|
}
|
||||||
|
|
||||||
fn normalize_admin_database_value(value: &Value) -> Value {
|
fn normalize_admin_database_value(value: &Value) -> Value {
|
||||||
match value {
|
match value {
|
||||||
Value::Array(items) if items.len() == 1 => normalize_admin_database_value(&items[0]),
|
Value::Array(items) if items.len() == 1 => normalize_admin_database_value(&items[0]),
|
||||||
@@ -1526,6 +1619,46 @@ mod tests {
|
|||||||
assert_eq!(response.rows[0].cells["points"], json!(12));
|
assert_eq!(response.rows[0].cells["points"], json!(12));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_admin_database_table_rows_sql_response_maps_recharge_order_enum_cells() {
|
||||||
|
let payload = json!([
|
||||||
|
{
|
||||||
|
"schema": {
|
||||||
|
"elements": [
|
||||||
|
{"name": {"some": "order_id"}},
|
||||||
|
{"name": {"some": "kind"}},
|
||||||
|
{"name": {"some": "status"}},
|
||||||
|
{"name": {"some": "paid_at"}}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"rows": [[
|
||||||
|
"recharge:user_00000001:1778757456811099:points_60",
|
||||||
|
[0, []],
|
||||||
|
[0, []],
|
||||||
|
[1, []]
|
||||||
|
]]
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
let response =
|
||||||
|
parse_admin_database_table_rows_sql_response("profile_recharge_order", 100, payload)
|
||||||
|
.expect("recharge order rows should parse");
|
||||||
|
|
||||||
|
let cells = &response.rows[0].cells;
|
||||||
|
assert_eq!(cells["kind"], json!("points"));
|
||||||
|
assert_eq!(cells["status"], json!("pending"));
|
||||||
|
assert_eq!(cells["paid_at"], json!(null));
|
||||||
|
assert_eq!(
|
||||||
|
response.rows[0].raw,
|
||||||
|
json!([
|
||||||
|
"recharge:user_00000001:1778757456811099:points_60",
|
||||||
|
"points",
|
||||||
|
"pending",
|
||||||
|
null
|
||||||
|
])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn build_admin_database_table_row_normalizes_optional_sats_values() {
|
fn build_admin_database_table_row_normalizes_optional_sats_values() {
|
||||||
let row = build_admin_database_table_row(
|
let row = build_admin_database_table_row(
|
||||||
|
|||||||
Reference in New Issue
Block a user