实现网站游戏评分与评价 (#561)
Project CI / AI game creator shell Rust crates (push) Successful in 1m43s
Project CI / AI game creator shell Rust lane 2/2 (push) Failing after 2m7s
Project CI / AI game creator shell Rust smoke (push) Successful in 2m55s
Project CI / AI game creator shell Rust lane 1/2 (push) Failing after 3m23s
Project CI / Frontend tests (push) Successful in 3m3s
Project CI / Repository checks (push) Successful in 3m42s
Project CI / AI game creator shell web tests (push) Successful in 2m3s
Project CI / Backend tests (push) Successful in 6m19s
Project CI / Native shell tests (push) Successful in 8m7s
Project CI / AI game creator shell Rust crates (push) Successful in 1m43s
Project CI / AI game creator shell Rust lane 2/2 (push) Failing after 2m7s
Project CI / AI game creator shell Rust smoke (push) Successful in 2m55s
Project CI / AI game creator shell Rust lane 1/2 (push) Failing after 3m23s
Project CI / Frontend tests (push) Successful in 3m3s
Project CI / Repository checks (push) Successful in 3m42s
Project CI / AI game creator shell web tests (push) Successful in 2m3s
Project CI / Backend tests (push) Successful in 6m19s
Project CI / Native shell tests (push) Successful in 8m7s
公开游戏详情页新增评分与评价:登录用户可提交并修改自己对每款游戏的唯一一条 1–10 分评价,评论可空且最多 4000 个 Unicode 码点;游客可分页浏览评价并查看平均分与评分人数。 - 新增私有评价表及共享 DTO/API,事务内校验游戏公开可见性、保证唯一性,并计算分页与全量评分统计;下架或封禁后关闭读写,重新公开及更新版本保留评价。 - 详情页支持编辑预填、取消、提交期间冻结输入、失败保留草稿,以及保存、翻页和账号/游戏切换时的旧响应隔离;复用现有认证与公共组件。 - 同步主规范、里程碑、实施计划、生成绑定、表目录和验收脚本。不扩展 AGC、外部 API、评论审核或评分缓存。 验证: - 本轮审查复跑前端定向测试 40 项、评价领域测试 2 项,均通过。 - 网站 typecheck、Rust/TypeScript DTO 对齐、SpacetimeDB schema、文档索引、编码与 diff 检查通过。 - 实施文档另记录本地隔离数据库/API 49 项及桌面/移动视口浏览器 smoke 通过;本轮审查未重跑这些运行时验收,也未运行完整测试套件。 发布说明:新增 SpacetimeDB 表,部署时需同步 module 与 API。尚未部署 dev/production,待用户验收。 Reviewed-on: https://git.genarrative.world/git/GenarrativeAI/Genarrative/pulls/561 Co-authored-by: Linghong <ink29535@proton.me> Co-committed-by: Linghong <ink29535@proton.me>
This commit was merged in pull request #561.
This commit is contained in:
@@ -10,7 +10,7 @@ use crate::creation_entry_config::{
|
||||
};
|
||||
|
||||
/// 后台 member 可被授予的一级 Tab 权限;账号管理仅 owner 可见,不进入该集合。
|
||||
pub const ADMIN_TAB_PERMISSIONS: [&str; 20] = [
|
||||
pub const ADMIN_TAB_PERMISSIONS: [&str; 21] = [
|
||||
"dashboard",
|
||||
"overview",
|
||||
"tables",
|
||||
@@ -27,12 +27,124 @@ pub const ADMIN_TAB_PERMISSIONS: [&str; 20] = [
|
||||
"editor-generation-pricing",
|
||||
"editor-showcase",
|
||||
"game-management",
|
||||
"game-reviews",
|
||||
"editor-assets",
|
||||
"agc-templates",
|
||||
"error-reports",
|
||||
"project-snapshots",
|
||||
];
|
||||
|
||||
/// 后台游戏评价的游戏选择查询,覆盖全部游戏可见状态。
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AdminGameReviewGamesQuery {
|
||||
pub query: Option<String>,
|
||||
pub page: Option<u32>,
|
||||
pub page_size: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AdminGameReviewGame {
|
||||
pub game_id: String,
|
||||
pub title: String,
|
||||
pub status: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AdminGameReviewGamesResponse {
|
||||
pub games: Vec<AdminGameReviewGame>,
|
||||
pub page: u32,
|
||||
pub page_size: u32,
|
||||
pub total: u64,
|
||||
pub total_pages: u64,
|
||||
}
|
||||
|
||||
/// 用户 ID 精确匹配;关键词与其他条件由后端按 AND 组合筛选。
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AdminGameReviewsQuery {
|
||||
pub game_id: Option<String>,
|
||||
pub user_id: Option<String>,
|
||||
pub keyword: Option<String>,
|
||||
pub status: Option<String>,
|
||||
pub page: Option<u32>,
|
||||
pub page_size: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AdminGameReviewGameInfo {
|
||||
pub title: String,
|
||||
pub status: String,
|
||||
}
|
||||
|
||||
/// 后台完整评价;管理原因只保存在后台操作记录,不进入普通用户投影。
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AdminGameReview {
|
||||
pub id: String,
|
||||
pub game_id: String,
|
||||
pub game: AdminGameReviewGameInfo,
|
||||
pub author: crate::game_distribution::GameDistributionAuthor,
|
||||
pub score: i64,
|
||||
pub comment: String,
|
||||
pub is_hidden: bool,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
/// 管理操作记录不复制评论正文;删除后依旧保留目标身份与原创建时间。
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AdminGameReviewOperation {
|
||||
pub id: String,
|
||||
pub review_id: String,
|
||||
pub game_id: String,
|
||||
pub user_id: String,
|
||||
pub review_created_at: String,
|
||||
pub action: String,
|
||||
pub admin_user_id: String,
|
||||
pub reason: Option<String>,
|
||||
pub created_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AdminGameReviewsResponse {
|
||||
pub reviews: Vec<AdminGameReview>,
|
||||
pub page: u32,
|
||||
pub page_size: u32,
|
||||
pub total: u64,
|
||||
pub total_pages: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AdminGameReviewDetailResponse {
|
||||
pub review: AdminGameReview,
|
||||
pub operations: Vec<AdminGameReviewOperation>,
|
||||
}
|
||||
|
||||
/// 原创建时间保护删除后重新评价的实例;管理员身份来自会话。
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AdminGameReviewModerationRequest {
|
||||
pub action: String,
|
||||
pub expected_created_at: String,
|
||||
#[serde(default)]
|
||||
pub reason: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AdminGameReviewModerationResponse {
|
||||
pub review: Option<AdminGameReview>,
|
||||
pub operation: AdminGameReviewOperation,
|
||||
pub replayed: bool,
|
||||
}
|
||||
|
||||
/// 私有工程快照的后台分页查询。
|
||||
#[derive(Clone, Debug, Default, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -1298,6 +1410,55 @@ mod tests {
|
||||
AdminUpdateAgcTemplateRequest,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn game_review_contract_uses_camel_case_and_nullable_operation_result() {
|
||||
use super::{
|
||||
AdminGameReviewModerationRequest, AdminGameReviewModerationResponse,
|
||||
AdminGameReviewOperation,
|
||||
};
|
||||
let request: AdminGameReviewModerationRequest = serde_json::from_value(json!({
|
||||
"action":"restore", "expectedCreatedAt":"2026-10-01T00:00:00Z",
|
||||
}))
|
||||
.unwrap();
|
||||
assert!(request.reason.is_none());
|
||||
assert_eq!(
|
||||
serde_json::to_value(request).unwrap()["expectedCreatedAt"],
|
||||
"2026-10-01T00:00:00Z"
|
||||
);
|
||||
for value in [
|
||||
json!({"action":1,"expectedCreatedAt":"2026-10-01T00:00:00Z"}),
|
||||
json!({"action":"restore","expectedCreatedAt":1}),
|
||||
json!({"action":"hide","expectedCreatedAt":"2026-10-01T00:00:00Z","reason":123}),
|
||||
] {
|
||||
assert!(serde_json::from_value::<AdminGameReviewModerationRequest>(value).is_err());
|
||||
}
|
||||
let response = serde_json::to_value(AdminGameReviewModerationResponse {
|
||||
review: None,
|
||||
replayed: true,
|
||||
operation: AdminGameReviewOperation {
|
||||
id: "operation-1".into(),
|
||||
review_id: "review-1".into(),
|
||||
game_id: "game-1".into(),
|
||||
user_id: "user-1".into(),
|
||||
review_created_at: "2026-10-01T00:00:00Z".into(),
|
||||
action: "restore".into(),
|
||||
admin_user_id: "admin-1".into(),
|
||||
reason: None,
|
||||
created_at: "2026-10-01T00:00:01Z".into(),
|
||||
},
|
||||
})
|
||||
.unwrap();
|
||||
assert!(response["review"].is_null());
|
||||
assert!(response["operation"]["reason"].is_null());
|
||||
assert_eq!(
|
||||
response["operation"]["reviewCreatedAt"],
|
||||
"2026-10-01T00:00:00Z"
|
||||
);
|
||||
assert_eq!(response["operation"]["adminUserId"], "admin-1");
|
||||
assert!(response["operation"].get("comment").is_none());
|
||||
assert!(ADMIN_TAB_PERMISSIONS.contains(&"game-reviews"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn agc_template_snapshot_uses_camel_case_and_assignable_tab_permission() {
|
||||
let value = serde_json::to_value(AdminAgcTemplateListResponse {
|
||||
|
||||
Reference in New Issue
Block a user