@@ -1,8 +1,8 @@
# 平台入口与玩法链路
> 更新时间:`2026-09-2 3`
> 更新时间:`2026-09-30 `
>
> 本文只 描述现役主站平台壳、图片画布编辑器和对应后端契约。未出现在现役 路由树、shared-contracts 或 SpacetimeDB module / bindings 中的入口与数据对象不属于本文范围 。
> 本文描述现役主站平台壳、图片画布编辑器和游戏分发合同;拟新增能力在各节单独标明状态。“网站游戏评分与评价合同”是待实现方案,不代表现役能力。当前实现以 路由树、shared-contracts 和 SpacetimeDB module / bindings 为准 。
## 现役平台入口
@@ -64,7 +64,7 @@
交付“AGC 一键提交游戏 / 网页上传游戏 ZIP → 后端收取真实发行包 → 校验与人工审核 → 主站发现、详情、游客在线游玩 → 更新与下架”的完整闭环。验收必须使用真实上传、真实存储、真实审核状态和隔离发行域名;静态演示卡片、metadata-only 请求或前端本地发布状态不能作为完成证据。
首版支持可以离线运行的静态 Web 游戏:HTML、JavaScript、CSS、JSON 与图片、字体、音视频。AGC 首先支持现有 npm/Vite Web 工程;网页入口允许上传符合相同包合同的 ZIP,不以游戏引擎名称限制普通静态产物。Godot/Cocos 工程源码、原生可执行文件、Wasm、服务端进程、外部 API、多人联机、云存档和跨版本存档迁移不在首版内。暂不做 评分、 评论、 关注、榜单、推荐算法、交易和创作者收入。
首版支持可以离线运行的静态 Web 游戏:HTML、JavaScript、CSS、JSON 与图片、字体、音视频。AGC 首先支持现有 npm/Vite Web 工程;网页入口允许上传符合相同包合同的 ZIP,不以游戏引擎名称限制普通静态产物。Godot/Cocos 工程源码、原生可执行文件、Wasm、服务端进程、外部 API、多人联机、云存档和跨版本存档迁移不在首版内。本发行合同 Version 0.2 不包含用户 评分与 评论,其扩展范围见下文“网站游戏评分与评价合同”; 关注、榜单、推荐算法、交易和创作者收入仍不在范围内 。
### 入口与产品体验
@@ -176,3 +176,122 @@
3. 是否接受首版离线静态包、无 Wasm/外网/持久存档的范围,以及建议包额度、7 天失败包保留和 60 秒缓存撤销窗口;公开/撤销版本及审核记录保留周期待定。
4. 是否接受平台同源路径发行(`/games/<gameId>/` + `sandbox="allow-scripts"` 的不透明来源隔离)替代独立发行域名:选择该方案后独立域名、通配 DNS/TLS 与 CDN 不再是上线门禁,存储区域与运营责任仍需确定。
5. 本节已提供页面行为、发行状态、真实上传、幂等/CAS 和 API 草案,足以评审完整业务;尚不足以直接实现持久化和部署,必须在对应里程碑评审前冻结表/索引/服务身份、额度/清理、最终响应 DTO 与发行基础设施配置。未经评审不建立 `ready` 实施计划,不把 proposed 标记为 accepted。
## 网站游戏评分与评价合同
| 字段 | 值 |
| --- | --- |
| Version | 1.0 |
| Status | proposed(方案待评审,未实现、未验收) |
| Date | 2026-09-30 |
| 入口 | 网站 `/games/detail?id=<gameId>` |
| 交付目标 | 登录用户对同一游戏保存并修改唯一一条评分与可选评论;所有访客分页浏览评价,并查看真实平均分与评分人数 |
| 里程碑 | [网站游戏评分与评价 ](./project-memory/plans/【里程碑】网站游戏评分与评价-2026-09-30.md ) |
### 范围与默认规则
- 本次只扩展网站游戏详情页、现有游戏分发后端与共享契约;复用平台账号,评分与评论合并为一条“评价”。
- 用户已明确要求:评分范围 1–10、评论可空且最多 4000 字符、可修改自己的评价、每用户每游戏最多一条、个人评价展示与编辑预填、公共列表分页且包含自己的评价。
- 用户已确认加入详情页平均分和评分人数:无人评分显示“暂无评分”,空评论计入评分人数,修改不增加人数,当前规模先由后端读取评价记录计算。
- 本方案采用以下评审默认值:评分只接受整数;每页默认 20 条;按首次提交时间倒序;取消首次填写清空草稿,取消编辑恢复原评价;评价跟随游戏身份,不随发行版本更新清空。
- 登录用户均可评价,包括游戏作者;不增加必须先游玩、消费或具有发布权限的条件。评价权限不复用 `game-distribution:publish` 发布灰度。
- 本次不增加 AGC 客户端评价界面、独立评价页面、游戏目录评分展示、评论回复/点赞/图片/Markdown、评价删除、评价审核或推荐排行;不新建通用评论框架、评分缓存或独立统计任务。
### 评分、评论与统计口径
1. 评分必选,取值为整数 1–10;首次填写时不预选分数,不能仅提交文字而不评分。
2. 评论为空字符串或未提供时均允许提交。纯空白评论保存为空字符串;普通评论保留首尾空格与换行。
3. 输入与提交前统一把 `CRLF` / `CR` 换行为 `LF` ,再按 Unicode 码点计数;空格、换行同样计入上限。前端与后端计数一致,不按 UTF-8 字节或 UTF-16 单元计数;组合 emoji 可能含多个码点。
4. 长度校验先于纯空白归一化,4000 字符允许,4001 字符拒绝;不静默截断后保存。输入框旁始终显示 `当前字数/4000` ,超限显示可读错误并阻止提交;中文输入法与粘贴同样按该口径处理。
5. 每个评价对应一个已认证账号和一个稳定 `gameId` ,不绑定 `versionId` 。分数与评论一起原子保存;修改已有记录,`createdAt` 不变,有内容变化时更新 `updatedAt` 。
6. 平均分等于所有有效评价分数之和除以评价人数,包含作者、当前用户和只评分的记录;每账号最多计一次。后端返回保留一位小数的平均分,页面显示如“8.2/10 · 26 人评分”。
7. 无评价时 `ratingCount=0` 、`averageScore=null` ,显示“暂无评分”;不能显示“0 分”或虚构默认分。修改评分重新计算平均分,人数保持不变;修改文字不改变均分。
8. 平均分与人数来自后端对该游戏评价的权威读取,不能由当前分页或前端累计计算。本次不持久化总分、人数或均分,不增加缓存及重算任务。
### 页面布局与交互
游戏详情保留“立即玩”和现有资料,在游戏介绍后增加“我的评价”“全部评价”两个区块;平均分与人数显示在详情资料区,并在评价区域共用同一份后端摘要。
| 状态 | 我的评价区域 | 操作结果 |
| --- | --- | --- |
| 未登录 | 显示“登录后评价”入口,不展示可提交表单 | 使用现有平台登录入口;公共列表和评分摘要仍可浏览 |
| 读取自己的评价中 | 显示加载状态 | 确认不存在已有评价前,不提前展示首次填写表单 |
| 已登录且没有评价 | 展示 1–10 分选择、可选评论输入框、字数统计、取消/提交按钮 | 首次输入的评分和文字仅为当前页面草稿 |
| 已登录且已有评价 | 展示自己的分数、评论、修改时间及“编辑评价”按钮,隐藏输入模块 | 空评论仍展示分数,正文不填造提示内容 |
| 编辑已有评价 | 展示输入模块,完整预填分数与评论 | 个人展示区域暂由编辑表单替代,公共列表仍保留该评价 |
| 提交中 | 保留表单及内容,显示提交状态并禁用评分选择、评论输入及取消/提交 | 冻结本次提交内容,防止重复点击和后续输入被成功响应收起表单时丢失;失败后恢复编辑,不把在途请求解释为已保存 |
| 提交成功 | 用后端权威记录显示自己的评价,隐藏输入模块 | 刷新平均分、人数与公共列表 |
| 读取或提交失败 | 给出错误与重试入口 | 读取失败不伪装成“没有评价”;提交失败保留草稿,不覆盖原评价 |
- 首次填写点击取消:清空评分与文字,保留首次填写表单,不产生后端写入;编辑点击取消:丢弃草稿,恢复上次成功读取/保存的个人评价。
- 首次提交成功后公共列表回到第一页;修改成功后刷新当前页,保留页码。提交响应中的个人记录与统计摘要先更新,分页读取失败单独提示重试,不回滚已成功保存的评价。
- 保存成功后,使此前发出的个人评价与公共列表读取失效;迟到响应不得覆盖已保存的个人记录、评分摘要或刷新后的列表。同一游戏内快速翻页或重复刷新时,各读取区域只接受其最新请求的结果,旧请求的成功、错误和加载结束均不得覆盖当前状态。
- 刷新页面重新读取后端保存结果,不自动保存或恢复未提交草稿;退出登录、切换账号或切换游戏时清空个人评价、草稿与旧错误,旧请求的迟到响应不得写入新上下文。
- 评论纯文本展示并保留换行,不执行 HTML、不解析 Markdown。移动端评分控件可换行,评论与按钮不被底部导航遮挡;控件支持键盘选择并提供可访问名称。
- 评分摘要、公共列表和个人评价分别承接加载/失败;评价区域故障不阻断已有游戏资料和游玩动作。摘要加载失败显示重试状态,不能误显示“暂无评分”。
- 优先复用共享动作按钮、状态提示、空态、输入与分页表现;存在跨页面重复时再抽取共享表现组件,领域规则仍留在后端。
### 公共评价列表与分页
- 游客和登录用户都能读取公开游戏的评价;列表展示作者公开昵称/头像、分数、评论、首次提交时间,编辑后标明修改时间。
- 只评分的评价仍进入列表与总数,不制造“无评论”正文;**不排除当前用户**。自己的评价可以在个人区域与公共列表各出现一次,公共列表内部只能出现一条。
- 使用服务端页码分页,`page` 从 1 开始,`pageSize` 默认 20、允许 1–50;页面提供上一页、下一页、当前页及总页数。前端不能一次取全后自行分页。
- 默认按 `createdAt` 倒序,同时间按稳定评价标识倒序;编辑不改变排序位置,不新增排序/筛选选项。
- 无记录时第一页返回空列表、`total=0` 、`totalPages=0` ,页面显示真实空态并禁用翻页;超出末页返回空列表,不把不存在的页作为服务端错误。
- 分页与统计在单次读取的一致快照中返回;不同翻页请求之间允许反映新提交,不承诺跨请求冻结整份评价集合。
### 数据、身份与持久化边界
新增游戏评价私有表,保存以下必要字段;实际 Rust 表定义与绑定在实现计划中确定:
| 逻辑字段 | 约束 |
| --- | --- |
| `reviewId` | 稳定唯一标识;通过 `gameId + userId` 的无歧义组合标识保证每对游戏/账号只占一个主键 |
| `gameId` | 已存在的稳定游戏身份;建立按游戏查询索引 |
| `userId` | 仅来自平台认证主体,不接受请求指定或转移归属 |
| `score` | 整数 1– 10 |
| `comment` | 归一化后的纯文本,最多 4000 Unicode 码点,可为空 |
| `createdAt` | 首次保存时间,修改时不变 |
| `updatedAt` | 最近一次实际内容修改时间 |
- 游戏必须与当前公开详情采用相同可见性判定,存在已公开可玩版本;未公开、作者下架或管理员封禁时,评价公开读取、个人读取和提交均返回 404。已有评价保留,游戏重新公开后可继续读取和编辑。
- `api-server` 先认证并取得用户 ID;SpacetimeDB 入口继续校验现有受信运行服务身份。数据库上下文中的调用者是 API 服务身份,不能把它误当成网页用户。
- 在同一数据库事务里检查游戏可见性并按唯一主键创建或更新;不得用 HTTP 层“先查是否存在,再生成随机 ID 新建”保证唯一性。并发首次提交、双击与重试最终都只有一条记录。
- 对同一条评价的并发修改采用后提交成功的事务覆盖,避免为该低风险编辑引入新版本锁或操作账本;重复提交相同内容返回已有记录,不变更人数和创建时间,也不制造修改时间。
- 公共作者资料由现有账号公开投影取得,不能由评论请求伪造昵称或头像;私有表不直接对浏览器订阅公开。日志不记录评论正文、Token 或私有账号字段。
- 新增表,不删除、改名、重排或修改现有游戏/版本字段;同步迁移登记、表目录、生成绑定与 schema 检查。现有评价数据不存在,无需旧数据回填;已有游戏无评价时自然返回空列表与“暂无评分”。
- 旧客户端和现有游戏目录、详情 DTO 不需要增加必填字段;评分摘要由新增评价接口提供。本次不扩展 `/api/external/v1` ,不改外部 OpenAPI。
- 规则与校验进入 `module-game-distribution` ,表与事务进入 `spacetime-module` ,读取经 `spacetime-client` facade, HTTP/鉴权进入现有 `api-server` 分发路由,DTO 同步 `shared-contracts` / `packages/shared` ;网站只持有展示和编辑草稿。
### HTTP 与 DTO 草案
以下接口均为**待实现**,路径以 `/api/game-distribution` 为前缀,返回沿用平台 `ok/data/error/meta` envelope。用户评价与既有后台游戏发行“审核”是不同对象,`/admin/api/game-distribution/reviews` 继续只表示发行审核队列。
| 方法与路径 | 身份 | 请求与成功响应 |
| --- | --- | --- |
| `GET /games/{gameId}/reviews?page=1&pageSize=20` | 游客可读 | `data={reviews,page,pageSize,total,totalPages,ratingSummary}` ;公共作者资料随每条评价返回 |
| `GET /games/{gameId}/my-review` | 登录用户 | `data={review}` ;未评价为 `review:null` ,不返回 404 |
| `PUT /games/{gameId}/my-review` | 登录用户 | `{score,comment}` ; `comment` 可省略,按空字符串保存;首次保存与修改均返回 `200` 、`data={review,ratingSummary}` |
- `review` 字段:`id` 、`gameId` 、`author:{id,name,avatarUrl}` 、`score` 、`comment` 、`createdAt` 、`updatedAt` ; `avatarUrl` 可为空。个人与公共评价使用同一展示形状,时间为统一 UTC 格式。
- `ratingSummary={averageScore:number|null,ratingCount:number}` ;公共列表 `total` 等于 `ratingCount` 。`totalPages` 由总数与实际 `pageSize` 得出;均分与人数覆盖该游戏全部评价,不只覆盖当前页。
- PUT 是“保存我的唯一评价”,不能指定目标用户或改他人记录;首次创建和更新不拆成不同接口,不接收任意 `reviewId` 定位写入。
- `400` 表示 JSON/字段类型、非整数分数或分页参数错误;`401` 表示未登录/认证失效;`404` 表示游戏不可公开访问;`422` 表示整数评分越界或评论超长;复用现有限流的 `429` 及可读 `5xx` 失败。
- 网络中断或响应丢失时保存结果可能未知,前端不能宣布“已提交”或删除原评价;保留草稿并可通过个人读取核对结果后重试。PUT 重试不会追加第二条记录。
- 公共与个人响应均不缓存过期的评价/评分摘要,使用 `Cache-Control: no-store` ;不混入发布灰度或扩大现有发行沙箱权限。
### 验收标准与证据
| 条款 | 必须获得的证据 |
| --- | --- |
| 首次评分与可选评论 | 1 分、10 分和空评论成功;未选择评分、0/11/非整数被拒,后端没有无效写入 |
| 字数与输入 | 中文、英文、emoji、空格与换行按统一口径计数;4000 成功、4001 拒绝,字数提示实时可见,粘贴/中文输入法不绕过上限 |
| 唯一与并发 | 同一用户同游戏的双击、重试与并发首次保存后仍只有一条;不同用户、不同游戏各自独立 |
| 修改与取消 | 已评价先显示个人卡片;编辑完整预填,取消保留旧值,提交更新同一记录;提交中评分、评论和操作按钮均禁用,失败恢复编辑且不丢草稿;首次取消不落库 |
| 公共分页与重复展示 | 至少 21 条真实评价验证翻页/总数/顺序/空态;自己的评价在个人区和所属公共页各出现一次;空评论评价可见,编辑不换位 |
| 均分与人数 | 无记录为 `null/0` ;仅评分计人数;改分只改变均分、不加人数;改评论不改变均分;统计基于所有页 |
| 身份与可见性 | 匿名读成功、写/读个人为 401;伪造用户不能修改他人;下架与封禁后读写 404、恢复公开后记录仍在 |
| 生命周期与故障 | 切换账号/游戏后旧响应不覆盖新状态;保存成功后旧读取不覆盖新记录与摘要,快速翻页/刷新只接受各区域最新读取结果;个人读取失败不误展首次表单,摘要失败不误显示无评分;评价失败不阻断游玩 |
| 页面与工程 | 桌面/移动真实浏览器可操作,评分可键盘选择;相关前后端定向测试、网站类型检查、真实 SpacetimeDB/API smoke、DTO/schema/绑定、编码/文档索引/diff 检查 |
本节仅完成方案文档,所有实现、测试、真实数据库与浏览器验收证据均待补。完整行为作为一个独立里程碑交付;评审通过后再创建仅覆盖该里程碑的实现计划,不把文档编写视为功能验收。