Update schema to replace sort_order index with display_id index Apply display_id ordering to global search, lists, and relations Update design documentation to reflect the new sorting behavior
1343 lines
100 KiB
Markdown
1343 lines
100 KiB
Markdown
# Pokopia Wiki
|
||
|
||
## 产品目标
|
||
|
||
- Pokopia Wiki 是一个面向 Pokopia 游戏资料的社区 Wiki。
|
||
- 所有人都可以浏览 Wiki 内容。
|
||
- 已注册并完成邮箱验证且拥有对应权限的用户可以创建、编辑、删除 Wiki 内容。
|
||
- 前台以 Home 首页、Pokedex(Main Game / Event)、Habitat Dex(Main Game / Event)、Collections(Main Game / Event / Ancient Artifacts)、材料单、每日 CheckList、Life、Automation、Dish、Events、Actions、Dream Island、Clothes 为主要浏览入口。
|
||
- Threads 是社区讨论入口,采用 Discord Forum + 聊天室混合形态;用户按 Channel 浏览 Thread,并在 Thread 内使用聊天室式消息流讨论。
|
||
- Home 首页路径为 `/`,用于聚合公开 Wiki 入口;Logo 导航回到 Home,用户可从 Home 进入核心资料、每日 CheckList、Life 和正在准备中的分区。
|
||
- 桌面端使用侧边栏导航,侧边栏可折叠为图标栏;移动端继续使用抽屉式侧边栏。
|
||
- 全局顶部导航栏承载语言切换、通知、User Profile 和登录 / 退出等账号操作;除 User Profile 可展示用户名外,顶部操作以图标按钮呈现。
|
||
- 全局顶部导航栏提供全站搜索。搜索结果按内容类型分组展示,覆盖 Pokemon、Habitats、Items、Ancient Artifacts、Recipes、Daily CheckList、公开可见的 Life Post 和公开用户 Profile;结果跳转到对应公开详情页、页面锚点或 `/profile/:id`。
|
||
- 管理入口用于维护全局配置、语言、系统文案、列表排序和每日 CheckList。
|
||
|
||
## 技术栈
|
||
|
||
- Monorepo:pnpm workspace,Node.js >= 22,TypeScript。
|
||
- 前端:Nuxt(`ssr: true`)、Vue、Vue Router、Vue I18n、Iconify。
|
||
- 后端:Node.js、Fastify、pg、PostgreSQL。
|
||
- 运维:Docker / docker compose。
|
||
- 依赖版本遵循现有 `package.json`,新增依赖时优先使用当前主流稳定版本,并保持项目结构简单。
|
||
|
||
## 全局设计原则
|
||
|
||
- `DESIGN.md` 是产品行为、数据结构和 API 暴露边界的单一事实来源。
|
||
- API 只返回业务需要的字段,不返回密码、token hash、验证 token、内部调试字段或不必要的元数据。
|
||
- 全局搜索 API 只返回公开浏览所需的最小结果字段:结果类型、ID、展示标题、目标 URL、可选摘要和可选图片;用户搜索结果只使用公开 Profile 所需的 `id`、`displayName` 和目标 URL,不返回邮箱、角色、权限、Referral、编辑审计、审核原因、token/hash、内部字段或调试信息。
|
||
- 用户界面只展示业务数据和设计内的文案,不展示提示词、计划、调试信息、字段内部名或修改说明。
|
||
- 可编辑 Wiki 内容必须记录创建者、最后编辑者、创建时间、最后编辑时间和编辑历史。
|
||
- 除 Pokemon 外,列表顺序由 `sort_order` 控制,默认按创建时间旧到新初始化,排序值按 10 递增以便后续插入和拖拽排序;Pokemon 列表按 Pokopia 展示 ID(`display_id`)升序展示,不提供手动排序。
|
||
|
||
## 国际化
|
||
|
||
- 前端使用 Vue I18n 管理界面文案,并通过 `X-Locale` 请求头告知后端当前语言。
|
||
- 前端当前语言保存在 `localStorage` 的 `pokopia_locale`。
|
||
- Nuxt SSR 运行时每个 Nuxt app/request 创建独立 Vue I18n 实例,避免跨请求共享 locale 或系统文案状态;服务端默认使用 `en`,客户端 hydration 后按 `pokopia_locale` 恢复用户语言。
|
||
- 后端默认语言为 `en`。
|
||
- 语言配置存储在 `languages`:
|
||
- `code`
|
||
- `name`
|
||
- `enabled`
|
||
- `is_default`
|
||
- `sort_order`
|
||
- 语言 code 格式为 `xx` 或 `xx-YY`,例如 `en`、`zh-CN`。
|
||
- 系统必须且只能有一个默认语言。
|
||
- 初始语言包含:
|
||
- `en`:English,默认语言
|
||
- `zh-CN`:简体中文
|
||
- 实体翻译存储在 `entity_translations`:
|
||
- `entity_type`
|
||
- `entity_id`
|
||
- `locale`
|
||
- `field_name`
|
||
- `value`
|
||
- 支持翻译的实体:
|
||
- Pokemon
|
||
- 特长
|
||
- Pokemon Types
|
||
- 喜欢的环境
|
||
- 喜欢的东西 / 标签
|
||
- 入手方式
|
||
- 物品(包含 Ancient Artifacts 视图中的物品)
|
||
- 地图
|
||
- 栖息地
|
||
- 每日 CheckList Task
|
||
- Game Version
|
||
- Dish Category
|
||
- Dish Flavor
|
||
- Dish
|
||
- 支持翻译的字段:
|
||
- `name`
|
||
- `title`
|
||
- `details`:Pokemon 和物品的介绍 / 说明
|
||
- `genus`:仅 Pokemon Genus 使用
|
||
- `effect`:Dish Category 的吃后效果
|
||
- `mosslaxEffect`:Dish 给 Mosslax 吃之后的效果
|
||
- 实体仍保留基础 `name`、`title`、`details` 或 `genus` 字段,默认语言内容以基础字段为准。
|
||
- API 返回展示名称时按当前语言解析,回退顺序为:请求语言翻译 -> 默认语言翻译 -> 基础字段。
|
||
- 编辑表单必须避免本地化 UI 覆盖基础名称;翻译字段只展示当前需要编辑的语言。
|
||
- 系统级文案独立于实体翻译,不进入 `entity_translations`。
|
||
- 系统级文案 key 由代码 catalog 维护,覆盖前端界面、后端错误提示和认证邮件模板。
|
||
- 系统级文案值存储在 `system_wording_values`,key 元信息存储在 `system_wording_keys`:
|
||
- `key`
|
||
- `module`
|
||
- `surface`:`frontend` / `backend` / `email`
|
||
- `description`
|
||
- `placeholders`
|
||
- `enabled`
|
||
- `locale`
|
||
- `value`
|
||
- 后端启动时同步代码 catalog,只补充缺失 key 和初始 value,不覆盖管理员已维护的 value。
|
||
- 系统级文案回退顺序为:请求语言 value -> 默认语言 value -> 代码内置 fallback。
|
||
- 系统级文案中的占位符必须与默认文案一致,例如 `{count}`、`{name}`;保存时校验,避免运行时插值失败。
|
||
- 前端组件必须通过 Vue I18n key 读取系统文案,不直接写用户可见硬编码文案;后续新增模块必须先在 catalog 中注册 wording key。
|
||
- 后端返回给前端的 user-facing 错误信息必须通过系统文案解析,不返回 token/hash、内部调试字段或未本地化的内部错误文本。
|
||
- 管理入口提供 System wordings 维护能力,可按语言、模块、端和缺失状态查看并编辑系统级文案。
|
||
|
||
## 用户与认证
|
||
|
||
- 用户可注册:
|
||
- 邮箱
|
||
- 显示名
|
||
- 密码
|
||
- 邮箱保存为小写。
|
||
- 密码只保存 hash。
|
||
- 注册后必须通过邮箱验证。
|
||
- 邮件发送使用 Resend:
|
||
- `RESEND_API_KEY`
|
||
- `EMAIL_FROM`
|
||
- `APP_ORIGIN` 或 `FRONTEND_ORIGIN`
|
||
- 认证邮件和密码重置邮件使用标准化 Pokopia Wiki 品牌 HTML 外壳;正文、按钮文案、兜底链接提示和纯文本版本仍通过 `surface=email` 的系统级文案维护。
|
||
- 后端从 Resend 邮件发送响应 headers 读取日/月发送额度和 rate limit 状态,并维护短期内存 snapshot;当 Resend 已报告额度接近用尽、额度耗尽或 API 限流时,认证邮件发送会暂时停止并返回本地化用户提示。
|
||
- Resend 额度保护不使用本项目自增发送计数;默认按 Free 计划 `100/day`、`3000/month` 和 5 封保留量判断,可通过 `RESEND_DAILY_QUOTA_LIMIT`、`RESEND_MONTHLY_QUOTA_LIMIT`、`RESEND_QUOTA_RESERVE`、`RESEND_QUOTA_SNAPSHOT_TTL_MINUTES` 调整。
|
||
- 验证邮件包含一次性验证链接。
|
||
- 验证 token 只保存 hash,并带过期时间和使用状态。
|
||
- 只有邮箱已验证的用户可以登录。
|
||
- 用户可请求重置密码:
|
||
- 重置请求只接收邮箱,并始终返回泛化成功信息,避免暴露邮箱是否已注册。
|
||
- 重置邮件包含一次性重置链接。
|
||
- 重置 token 只保存 hash,并带过期时间和使用状态。
|
||
- 密码重置成功后不自动登录,并删除该用户已有 session。
|
||
- 登录页提供 Remember me:
|
||
- 未勾选时 session 有效期为 1 天。
|
||
- 勾选时 session 有效期为 30 天。
|
||
- SSR 认证使用 HTTP-only cookie session:
|
||
- 登录成功后后端设置 HTTP-only `pokopia_session` cookie;cookie 只保存明文 session token,数据库只保存 session token hash。
|
||
- 登录响应只返回当前用户必要字段,不返回明文 session token、session token hash 或内部 session 元数据。
|
||
- Remember me 通过 HTTP-only session cookie 有效期实现:未勾选时有效期为 1 天,勾选时有效期为 30 天。
|
||
- 受保护 API 只接受 HTTP-only cookie session,不接受前端 JavaScript 保存的 legacy Bearer token。
|
||
- 前端 API 请求携带 credentials,以便浏览器自动发送 HTTP-only session cookie;JavaScript 不读取该 cookie。
|
||
- 用户可退出登录,退出时删除对应 session 并清除 HTTP-only session cookie。
|
||
- 对外用户字段只包含必要信息:
|
||
- 当前用户:`id`、`email`、`displayName`、`emailVerified`
|
||
- 编辑署名:`id`、`displayName`
|
||
- User Profile:
|
||
- 登录用户可通过 `/profile` 查看自己的账号资料、邮箱验证状态、Referral 信息和公开主页内容。
|
||
- 任意用户可通过 `/profile/:id` 访问其他用户的公开 Profile。
|
||
- 公开 Profile 展示用户公开摘要、Life Feeds、Wiki 贡献统计、Like / Reaction 过的 Life Post 和评论过的内容。
|
||
- 用户可 Follow 其他用户;Follow 是单向关系,双方互相 Follow 时在展示层视为 Friends。
|
||
- Friend 不单独存储为独立关系,始终由双向 Follow 派生,避免双写不一致。
|
||
- 公开 Profile 展示 Followers、Following 和 Friends 数量;登录用户查看其他用户 Profile 时可看到自己与对方的关系状态:未关注、已关注、被对方关注或 Friends。
|
||
- 登录且邮箱已验证并拥有 `users.follow` 权限的用户可以 Follow / Unfollow 其他用户;用户不能 Follow 自己。
|
||
- Profile 的 Feeds 和 Reactions 中可从 Life Post 的 Reaction 汇总或 Reaction 活动打开公开 Reaction 用户列表 Modal。
|
||
- Profile 使用 Tabs 组织:Feeds、Contributions、Reactions、Comments;仅自己的 `/profile` 额外展示 Account。
|
||
- Contributions、Reactions、Comments 在对应 Tab 内提供二级分类:Contributions 可按主要内容类型或配置类查看,Reactions 可按 reaction 类型查看,Comments 可按 Life / Wiki discussion 来源查看。
|
||
- 公开用户摘要只包含 `id`、`displayName` 和公开展示需要的加入时间;不公开邮箱、角色、权限、Referral Code、邀请链接、session、token/hash 或内部审计 payload。
|
||
- 当前用户可在自己的 `/profile` Account Tab 更新 `displayName`、查看 Referral 信息、复制 Referral 邀请链接,并修改密码;当前版本不支持头像或邮箱修改。
|
||
- 当前用户自己的 Profile 顶部摘要区可显示简化 Referral Code 和 Copy Link 入口;完整 Referral 卡片保留 Referral Code、邀请链接复制入口和有效邀请数量;这些字段不在公开 Profile 展示。
|
||
- 修改密码必须提交当前密码和新密码;成功后更新 password hash、作废未使用的密码重置 token,并保留当前 session、删除该用户其他 session。
|
||
- 修改密码 API 只返回本地化结果 message,不返回 user、session、token/hash 或内部审计 payload。
|
||
- 更新显示名后,API 仍只返回当前用户必要字段,不返回 session、token/hash、内部审计或调试数据。
|
||
- 显示名用于编辑署名、讨论和 Life 内容作者展示。
|
||
|
||
## 用户角色与权限
|
||
|
||
- Pokopia 使用 RBAC 权限模型:
|
||
- 用户通过 `user_roles` 关联到一个或多个角色。
|
||
- 角色通过 `role_permissions` 关联到一个或多个权限。
|
||
- 后端只按启用状态的权限 key 做访问控制;前端只用于展示或隐藏操作入口,不能作为权限边界。
|
||
- 邮箱验证仍是所有写入能力的基础门槛;未验证用户即使拥有角色也不能执行受保护写操作。
|
||
- 对外当前用户字段只包含必要信息:
|
||
- `id`
|
||
- `email`
|
||
- `displayName`
|
||
- `emailVerified`
|
||
- `roles`:只包含 `id`、`key`、`name`、`level`
|
||
- `permissions`:当前用户启用权限 key 列表
|
||
- 编辑署名仍只展示用户 `id` 和 `displayName`,不展示角色、权限、邮箱、token/hash 或内部元数据。
|
||
- 权限记录存储在 `permissions`:
|
||
- `key`:稳定权限 key,例如 `pokemon.create`
|
||
- `name`
|
||
- `description`
|
||
- `category`
|
||
- `enabled`
|
||
- `system_permission`:系统初始化权限标记,仅用于管理端识别默认权限
|
||
- 角色记录存储在 `roles`:
|
||
- `key`
|
||
- `name`
|
||
- `description`
|
||
- `level`:用于表达管理层级,数值越大层级越高
|
||
- `enabled`
|
||
- `system_role`:系统初始化角色标记,仅用于管理端识别默认角色
|
||
- 初始角色包含:
|
||
- `owner`:最高层级,拥有所有系统权限。
|
||
- `admin`:拥有内容、系统配置、用户、角色和权限管理能力。
|
||
- `editor`:拥有主要 Wiki 内容创建、更新、排序、上传和社区互动能力,不默认拥有删除、用户、角色或权限管理能力。
|
||
- `member`:拥有 Life、讨论发布和删除本人内容的社区能力。
|
||
- `viewer`:无写入权限,仅用于显式只读分组。
|
||
- Bootstrap 规则:
|
||
- 启动时若已有已验证用户但没有任何 `owner` 用户,系统自动将最早完成验证的用户加入 `owner` 角色。
|
||
- 若系统还没有 `owner` 用户,首个完成邮箱验证的用户自动加入 `owner` 角色。
|
||
- 已完成邮箱验证且没有任何角色的用户默认加入 `editor` 角色;已有角色关系的用户不被覆盖。
|
||
- 系统初始化只补齐默认角色、默认权限、Owner 关联和无角色已验证用户的默认 Editor 关联;不覆盖管理员对默认角色/权限元数据或角色权限分配的配置。
|
||
- 新建权限会自动关联到 `owner` 角色,确保 Owner 始终拥有可用权限全集;`owner` 角色的权限分配不能在管理端被手动删改。
|
||
- 系统必须始终至少保留一个拥有 `admin.permissions.update` 且可管理权限的有效用户;核心 RBAC 管理权限(`admin.access`、`admin.users.*`、`admin.roles.*`、`admin.permissions.*`)不能被禁用或删除;不能删除最后一个 Owner,不能移除最后一个 Owner 的关键权限能力。
|
||
- 权限管理能力本身也通过权限控制;只有拥有相应管理权限的用户可以查看、新增、编辑、删除权限、角色和用户角色关系。
|
||
- 用户角色分配必须同时满足层级边界:
|
||
- `PUT /api/admin/users/:id/roles` 的基础权限为 `admin.users.update`。
|
||
- 调用者只能分配或移除 `roles.level` 严格低于自己最高启用角色等级的角色。
|
||
- `owner` 角色只能由当前拥有启用 `owner` 角色且拥有 `admin.users.assign-owner` 权限的调用者分配或移除。
|
||
- 非 Owner 即使拥有 `admin.users.update` 或自定义高等级角色,也不能分配或移除 `owner` 角色。
|
||
- 管理 API 只返回权限管理所需字段,不返回密码、session token hash、verification/reset token hash、内部审计 payload 或调试字段。
|
||
|
||
## Admin Data Tools
|
||
|
||
- Admin Data Tools 用于在管理端导出、导入和清空指定 Wiki 内容域数据。
|
||
- Data Tools 只支持固定业务范围,不提供任意 SQL、任意表名输入或网页数据库控制台能力。
|
||
- 权限:
|
||
- `admin.data.export`:可导出内容数据 bundle。
|
||
- `admin.data.import`:可导入内容数据 bundle,并可执行 Wipe。
|
||
- 初始默认只有 `owner` 拥有 Data Tools 权限;如需开放给其他角色,必须通过权限管理显式授予。
|
||
- Data Tools 支持范围:
|
||
- Pokemon
|
||
- Habitats
|
||
- Items
|
||
- Ancient Artifacts
|
||
- Recipes
|
||
- Daily CheckList
|
||
- Items 与 Recipes 存在依赖关系;选择 Items 进行导出、导入或 Wipe 时,系统必须自动同时纳入 Recipes,前端确认内容也必须显示 Recipes。
|
||
- Wipe 行为:
|
||
- 删除所选范围的主数据、关联数据、实体翻译、编辑历史、图片上传记录和实体讨论评论。
|
||
- Wipe Pokemon 会删除 Pokemon 及其属性 / 特长 / 喜欢的东西 / 掉落关联 / Trading 观察,并移除栖息地中的 Pokemon 出现配置,但不删除栖息地本身。
|
||
- Wipe Habitats 会删除栖息地、栖息地配方项和 Pokemon 出现配置,但不删除 Pokemon、Items 或 Maps。
|
||
- Wipe Items 会先删除 Recipes,再删除物品、物品入手方式 / 喜欢的东西关联、栖息地配方项、Pokemon 掉落关联和 Trading 观察。
|
||
- Wipe Ancient Artifacts 会清空物品上的 Ancient Artifact 分类并删除对应 Ancient Artifact 讨论;物品本身仍保留在 Items / Event Items 中。
|
||
- Wipe Recipes 会删除材料单、材料项和入手方式关联,但不删除 Items。
|
||
- Wipe Daily CheckList 会删除清单任务和任务翻译 / 编辑历史。
|
||
- 对被清空的 identity 主表重置自增 ID;Pokemon 内部 ID 不是 identity,未关联官方 data 的自定义 Pokemon 系统分配区间仍按当前数据库最大值继续。
|
||
- Export 行为:
|
||
- 导出为版本化 JSON bundle,包含 `version`、`exportedAt`、`scopes` 和对应范围数据。
|
||
- JSON bundle 用于系统导入,不作为前台展示内容。
|
||
- 导出包含所选范围的主数据、关联数据、实体翻译、编辑历史、图片上传记录和实体讨论评论。
|
||
- 导出必须包含对应 Wipe 会移除的跨范围关联行,例如 Pokemon 出现配置、Pokemon 掉落、Trading 观察和栖息地配方项;导入这些关联时,引用的另一侧实体必须已存在。
|
||
- JSON 不包含上传文件本身;`backend_uploads` volume 需要单独备份。
|
||
- Import 行为:
|
||
- 当前只支持 Replace selected scopes:导入前先 Wipe bundle 中包含的范围,再在同一事务中还原 bundle 数据。
|
||
- Import 不自动覆盖系统配置、语言、用户、角色、权限、系统文案或 Life 内容。
|
||
- 导入数据引用的 System config、Languages、Users 或上传文件路径必须已存在;缺失依赖会导致导入失败并回滚。
|
||
- Import 完成后重置相关 identity sequence 到当前最大 ID 之后。
|
||
- Data Tools 额外支持 Items CSV 导入,用于在 Wipe Items 后按 CSV 顺序批量新增普通 Items;CSV 导入只新增 Items,不自动 Wipe,不创建 Recipes、入手方式、标签或翻译。
|
||
- Items CSV 必须包含 `name`、`category`、`description`、`image_file_name`、`not_registered_in_collection`、`cannot_grow_again_today` 列。
|
||
- Items CSV 的 `category` 必须匹配系统固定物品分类;支持 `Misc.` 匹配内置 `Misc`,其他值按固定分类英文名匹配。
|
||
- Items CSV 导入时,`description` 写入物品介绍;若 `not_registered_in_collection` 为 true,追加 `Note: Not registered in collection`;若 `cannot_grow_again_today` 为 true,追加 `Note: Cannot have Grow used on it again today`;原介绍非空时 Note 前使用换行分隔。
|
||
- Items CSV 导入时,图片路径保存为 `/pokopia/items/{image_file_name}`,API 对外图片 URL 解析为 `https://pokesprite.tootaio.com/pokopia/items/{image_file_name}`。
|
||
- Data Tools 额外支持 Habitats CSV 导入,用于在 Wipe Habitats 后按 CSV 顺序批量新增 Habitats;CSV 导入只新增 Habitats,不自动 Wipe,不创建配方项、Pokemon 出现配置或翻译。
|
||
- Habitats CSV 必须包含 `id`、`name`、`image_file_name` 列。
|
||
- Habitats CSV 的 `id` 仅用于识别导入行与 Event 标记,不写入数据库主键;`id` 前缀为 `E` 或 `E-` 时导入为 Event Habitat,否则导入为 Main Game Habitat。
|
||
- Habitats CSV 导入时,图片路径保存为 `/pokopia/habitats/{image_file_name}`,API 对外图片 URL 解析为 `https://pokesprite.tootaio.com/pokopia/habitats/{image_file_name}`。
|
||
- 前端 JSON bundle Import 和 Wipe 必须使用确认 Modal,并要求输入固定确认词后才能执行;Items CSV 和 Habitats CSV 导入只新增对应内容,不执行删除,可直接从 CSV 文件选择触发。
|
||
|
||
## Referral
|
||
|
||
- Referral 是账号功能,用于让已注册用户邀请新用户加入 Pokopia Wiki。
|
||
- 每个用户都有一个稳定的 Referral Code:
|
||
- 由系统生成。
|
||
- 全局唯一。
|
||
- 只包含大写英文字母和数字。
|
||
- 现有用户在首次读取 Referral 信息或重新注册未验证账号时自动补齐。
|
||
- 登录用户可在 `/profile` Account Tab 查看自己的 Referral Code、邀请链接复制入口和有效邀请数量。
|
||
- 邀请链接使用前端注册页路径:`/register?ref=CODE`。
|
||
- 注册页支持:
|
||
- 从 `ref` query 自动填入 Referral Code。
|
||
- 用户手动输入 Referral Code。
|
||
- Referral Code 可为空。
|
||
- 注册提交时后端校验 Referral Code:
|
||
- 无效 Referral Code 拒绝注册并返回本地化错误。
|
||
- 用户不能使用自己的 Referral Code;如邮箱已存在且该账号已有 Referral Code,注册时不能将自己设为邀请人。
|
||
- 已存在未验证账号重新注册时,不覆盖已有邀请关系。
|
||
- Referral 只有在被邀请用户完成邮箱验证后才计入有效邀请数量。
|
||
- Referral 不改变现有邮箱验证要求;用户仍必须验证邮箱后才能登录和编辑。
|
||
- 当前版本不提供积分奖励、排行榜、邀请邮件发送、邀请制注册限制、后台统计或公开邀请人资料页。
|
||
- Referral API 对外只返回当前用户自己的 Referral 摘要,不返回被邀请用户邮箱、token/hash、内部审计字段或被邀请用户明细。
|
||
|
||
## Notifications
|
||
|
||
- Notifications 用于让已登录用户接收与自己相关的社区互动和审核结果。
|
||
- 通知持久化存储,用户离线期间产生的通知会在下次登录后继续可见。
|
||
- 通知和审核状态实时更新可以走 WebSocket;WebSocket 连接使用短期一次性 ticket,不把 session token 放入 WebSocket URL。
|
||
- AI 审核从 `reviewing` 变更为 `approved`、`rejected` 或 `failed` 后,前端当前可见的对应 Life Post、Life Comment 或实体讨论评论状态、语言区和可展示的审核原因详情应通过 WebSocket 直接更新,不要求用户刷新页面。
|
||
- 通知范围:
|
||
- 用户被别人 Follow 时,通知被 Follow 的用户;同一用户重复 Follow 同一目标时合并更新同一通知。
|
||
- Life Post 收到审核通过后的顶层评论时,通知 Life Post 作者。
|
||
- Life Comment 收到审核通过后的回复时,通知父评论作者。
|
||
- 实体讨论评论收到审核通过后的回复时,通知父评论作者。
|
||
- Life Post 收到 Reaction 时,通知 Life Post 作者;同一用户对同一 Life Post 的 Reaction 通知合并更新。
|
||
- Life Post、Life Comment、实体讨论评论的 AI 审核完成为 `approved`、`rejected` 或 `failed` 时,通知内容作者。
|
||
- 用户自己的操作不通知自己。
|
||
- 顶层实体讨论评论当前没有单一明确内容所有者,不默认通知 Wiki 实体创建者或最后编辑者;讨论回复仍通知父评论作者。
|
||
- 普通用户只能读取、标记自己收到的通知。
|
||
- 通知 API 返回字段只包含展示所需内容:
|
||
- `id`
|
||
- `type`
|
||
- 触发用户必要署名 `actor`:只包含 `id` 和 `displayName`,系统审核结果可为 `null`
|
||
- 目标跳转信息 `target`:只包含目标类型、ID、路径和必要业务引用
|
||
- `reactionType`
|
||
- `moderationStatus`
|
||
- `moderationReason`:仅当审核结果为 `rejected` 或 `failed` 时可包含面向用户的简短原因详情;`approved` 时为 `null`
|
||
- `readAt`
|
||
- `createdAt`
|
||
- `updatedAt`
|
||
- 通知 API 不返回邮箱、角色、权限、session、token/hash、AI prompt、模型响应、内部审核错误、错误堆栈、调试字段或内部审计 payload。
|
||
- 前端在主导航登录区展示通知入口、未读数量和通知列表;点击通知后标记已读并跳转到对应 Life Post 或 Wiki 详情页。
|
||
- Follow 对象发布 Life Post 的动态属于 Following Feed,不进入 Notifications,不产生未读数量,也不需要标记已读。
|
||
- Thread Follow 的未读状态在 Threads 自身侧边栏和 Thread 列表展示,不进入全局 Notifications,不影响 NotificationBell 未读数量。
|
||
|
||
## 滥用防护与限流
|
||
|
||
- 后端使用 `@fastify/rate-limit` 和应用内用户级计数在应用层执行限流;默认内存存储适用于当前单实例运行,后续多实例部署需要切换到共享存储或反向代理层限流。
|
||
- Fastify 默认不信任代理转发 IP;部署在可信反向代理后方时,可设置 `TRUST_PROXY=true`,让 IP 限流使用代理解析后的客户端 IP。
|
||
- 限流 key 不对外暴露;邮箱限流使用规范化小写邮箱生成内部 key,已登录用户限流使用当前登录用户 ID,路由限流使用 HTTP method + route pattern。
|
||
- 触发限流时 API 返回 429 和本地化通用错误文案,并带 `Retry-After` 与 rate limit headers;响应不得返回邮箱、用户 ID、内部 key、token/hash 或调试信息。
|
||
- 可配置的已登录用户限流存储在 `rate_limit_settings`:
|
||
- `settings`:JSON object,保存各用户级限流策略的 `maxRequests`、`timeWindowSeconds` 和 `cooldownSeconds`
|
||
- `updated_by_user_id`
|
||
- `created_at`
|
||
- `updated_at`
|
||
- 管理端 Access 分组提供 Rate limits 设置区;查看需要 `admin.rate-limits.read`,更新需要 `admin.rate-limits.update`。
|
||
- 已登录用户级限流策略仅按用户 ID 计数,不再叠加写入路由 IP 限流或用户 + 路由写入限流;认证入口和受保护路由的 IP 防护仍保留。
|
||
- 认证入口限流:
|
||
- 注册、登录、验证邮箱、请求重置密码、提交重置密码均按 IP + 路由限制为 20 次 / 10 分钟。
|
||
- 登录额外按邮箱限制为 5 次 / 15 分钟。
|
||
- 注册额外按邮箱限制为 3 次 / 1 小时。
|
||
- 请求重置密码额外按邮箱限制为 3 次 / 1 小时,并按 IP + 路由限制为 10 次 / 15 分钟。
|
||
- 提交重置密码额外按 IP + 路由限制为 10 次 / 15 分钟。
|
||
- 已登录保护路由按 IP + 路由限制为 120 次 / 10 分钟,避免单一来源反复触发鉴权查询。
|
||
- 用户账号资料写入默认按用户 ID 限制为 20 次 / 1 小时,并有 5 秒冷却时间。
|
||
- 管理写入(System config 配置项、用户角色、角色、权限、语言、系统文案、AI 审核设置和限流设置)默认按用户 ID 限制为 120 次 / 1 小时,并有 2 秒冷却时间。
|
||
- Wiki 内容写入(Pokemon、物品、材料单、栖息地、每日 CheckList 和排序)默认按用户 ID 限制为 120 次 / 1 小时,并有 2 秒冷却时间。
|
||
- 上传默认按用户 ID 限制为 20 次 / 1 小时,并有 30 秒冷却时间。
|
||
- Community 写入:
|
||
- Life Post、Life 评论、Wiki 讨论评论、Thread / Thread Message 和对应删除 / 更新操作默认按用户 ID 限制为 60 次 / 1 小时,并有 5 秒冷却时间。
|
||
- Life reaction 和 Thread reaction / follow 写入默认按用户 ID 限制为 120 次 / 1 小时,并有 1 秒冷却时间。
|
||
- Pokemon Fetch 数据和图片候选查询默认按用户 ID 限制为 60 次 / 10 分钟,并有 1 秒冷却时间。
|
||
|
||
## Community 编辑与审计
|
||
|
||
- 已验证且拥有对应权限的用户可以通过前台或管理入口编辑 Wiki 内容。
|
||
- 新增、修改、删除 Wiki 内容时必须写入审计信息。
|
||
- 可编辑实体包含:
|
||
- Pokemon
|
||
- 栖息地
|
||
- 物品
|
||
- 材料单
|
||
- 每日 CheckList Task
|
||
- 全局配置项
|
||
- 主要可编辑表包含:
|
||
- `created_by_user_id`
|
||
- `updated_by_user_id`
|
||
- `created_at`
|
||
- `updated_at`
|
||
- `sort_order`
|
||
- 详细编辑历史存储在 `wiki_edit_logs`:
|
||
- `entity_type`
|
||
- `entity_id`
|
||
- `action`:`create` / `update` / `delete`
|
||
- `user_id`
|
||
- `changes`
|
||
- `created_at`
|
||
- 详情页展示最后编辑者、最后编辑时间和编辑历史面板。
|
||
- 编辑历史中的用户信息只展示必要署名,不暴露邮箱、token、hash 或内部元数据。
|
||
- 非 Pokemon 列表排序操作仍更新列表顺序、最后编辑者和最后编辑时间,但 `sort_order` / Sort order 字段变更不写入或展示在详情页编辑历史面板中。
|
||
- 编辑署名、编辑历史署名、Life 作者和讨论作者可链接到对应公开 Profile。
|
||
|
||
## Wiki 图片上传
|
||
|
||
- 已验证且拥有对应上传权限的用户可以为以下 Wiki 实体上传图片:
|
||
- Pokemon
|
||
- 物品图标
|
||
- 栖息地
|
||
- 上传图片只支持 `png`、`jpg/jpeg`、`webp`、`gif`。
|
||
- 上传图片由服务端保存到受控上传目录,不接受任意外部 URL,也不信任客户端传入的最终文件路径。
|
||
- 上传路径由服务端按实体类型、实体展示名称和时间戳生成,格式示例:
|
||
- `items/甜蜜蜜/20260501002000.png`
|
||
- `pokemon/Pikachu/20260501002000.png`
|
||
- `habitats/森林/20260501002000.png`
|
||
- 路径中的实体名称仅用于资源归档和可读性,实体关联仍以数据库 ID 为准。
|
||
- 每次上传都会写入 `entity_image_uploads` 历史记录:
|
||
- `entity_type`
|
||
- `entity_id`
|
||
- `entity_name`
|
||
- `path`
|
||
- `original_filename`
|
||
- `mime_type`
|
||
- `byte_size`
|
||
- `created_by_user_id`
|
||
- `created_at`
|
||
- 实体表只保存当前显示图片的相对路径;历史上传记录不会因为切换当前图片而删除。
|
||
- 公共 API 对外返回图片上传历史只包含:`id`、`path`、`url`、`uploadedAt` 和上传者必要署名 `uploadedBy`;不返回 `entity_name`、原始文件名、MIME、文件大小、服务器绝对文件路径或内部存储元数据。若编辑接口确需实体关联,只能在受保护编辑接口返回 `entityId`。
|
||
- 图片上传本身不直接改变实体内容;用户仍需保存实体编辑表单后,当前图片选择才成为实体行为并写入现有编辑审计。
|
||
- Docker 运行时上传目录必须使用 volume 持久化,避免重新 build 后丢失用户上传图片。
|
||
|
||
## 实体讨论
|
||
|
||
- Pokemon、物品、材料单、栖息地详情页支持讨论。
|
||
- 所有人都可以浏览实体讨论。
|
||
- 已注册并完成邮箱验证且拥有 `discussions.comments.create` 权限的用户可以发表评论,并回复顶层评论。
|
||
- 讨论回复只支持一层回复,不做无限嵌套。
|
||
- 评论作者拥有 `discussions.comments.delete` 权限时可以删除自己的评论;拥有 `discussions.comments.delete-any` 权限的用户可以删除其他用户评论;删除后正文不再展示,已有回复保留在原位置。
|
||
- 被删除实体的讨论会随实体删除一并清理。
|
||
- 讨论列表按顶层评论分页读取,支持 `limit` / `cursor`;每页顶层评论携带其一层回复,响应包含 `items`、`nextCursor`、`hasMore`、`total`。
|
||
- 讨论列表支持 `sort`:`oldest` 默认按创建时间正序;`latest` 按创建时间倒序;`most-liked` 按点赞数倒序;`most-replied` 按直接回复数倒序;同分时使用创建时间和 ID 保持稳定排序。排序只作用于顶层评论,回复始终按创建时间正序展示。
|
||
- 已注册并完成邮箱验证且拥有 `discussions.comments.like` 权限的用户可以点赞或取消点赞审核通过且未删除的实体讨论评论;每个用户对每条评论最多 1 个 Like。
|
||
- 实体讨论评论和回复必须进入 AI 审核;未审核通过的评论不向普通访客公开。
|
||
- 作者本人和拥有 `discussions.comments.delete-any` 权限的管理用户可看到相关评论的审核状态,并可触发重新审核。
|
||
- 审核状态包括:`unreviewed`、`reviewing`、`approved`、`rejected`、`failed`;前端面向用户展示为未审核、审核中、审核通过、审核不通过、审核失败。
|
||
- 新增或更新审核目标时先进入不可公开状态;只有 AI 审核通过后才进入普通公开讨论列表。
|
||
- 审核失败不等于审核通过;失败内容保持不可公开,用户可重新审核。
|
||
- `reviewing` 表示审核正在进行中,前端不展示重新审核入口;只有 `unreviewed`、`rejected` 和 `failed` 这类非进行中且未通过状态可触发重新审核。
|
||
- `rejected` 和 `failed` 可向作者本人或有管理权限的用户展示简短原因详情;`approved` 和 `reviewing` 不展示原因。
|
||
- AI 审核会自动识别评论适合的语言区,语言区使用启用状态的 `languages.code`,但不影响系统 UI 语言。
|
||
- 讨论列表支持按语言区读取;`language=all` 或不传语言参数时读取全部已公开语言区,传入具体语言 code 时只读取对应语言区。
|
||
- 讨论内容是用户生成内容,正文按作者输入展示,不进入 `entity_translations`。
|
||
- API 对外只返回评论作者的 `id` 和 `displayName`。
|
||
- API 对外返回讨论评论的 `likeCount`、`replyCount` 和当前用户自己的 `myLiked`;不返回点赞用户列表、邮箱、角色、权限或内部审计。
|
||
- API 可返回作者本人或管理用户处理评论所需的审核状态、语言区和 `rejected` / `failed` 原因详情;不返回邮箱、token/hash、内部调试字段、AI prompt、模型原始响应、内部审核错误、错误堆栈、`deleted_at`、`deleted_by_user_id` 等内部字段。
|
||
|
||
## AI 审核
|
||
|
||
- Life Post、Life Comment、实体讨论评论和实体讨论回复都是用户生成内容,必须经过 AI 审核。
|
||
- AI 审核支持 Gemini-compatible `generateContent` API 和 OpenAI-compatible `chat/completions` API;End Point、API Key、模型、API 格式、鉴权方式、RPM 限流和启用状态可由拥有 `admin.ai-moderation.*` 权限的管理员配置。
|
||
- 默认使用 Gemini-compatible `generateContent` API 和 Bearer token 鉴权,以兼容 NewAPI 等转发服务;鉴权方式仍支持 Gemini 原生 query `key`。
|
||
- 后端日志必须对 API Key 脱敏,且不回显给前端。
|
||
- 默认 End Point 为 `https://ai.example.com/v1beta`;API Key 不写入前端包,不回显给前端,管理 API 只返回是否已配置。
|
||
- 管理配置存储在后端受控表中;API 不返回 API Key 明文、模型原始响应、prompt、请求体、内部错误堆栈或调试字段。
|
||
- 后端日志可以记录安全脱敏后的第三方 HTTP 状态和错误摘要,用于排查 Endpoint、模型或鉴权配置问题;日志不得包含 API Key、审核 prompt 或用户正文。
|
||
- 服务端审核请求必须限流,按配置的每分钟请求数串行发送,避免触发第三方 API RPM 限制。
|
||
- 为节省 Token:
|
||
- 审核只发送待审核正文、允许的语言 code 和最小必要规则,不发送用户资料、页面上下文、审计 payload 或无关业务数据。
|
||
- 对相同正文和相同 API 配置/模型使用内容 hash 缓存审核结果,避免重复调用 AI。
|
||
- 审核请求使用结构化 JSON 输出、低温度和较小输出 token 上限。
|
||
- 安全要求:
|
||
- 用户正文必须作为不可信内容处理,不能作为系统指令或开发指令执行。
|
||
- 不允许通过用户正文关闭、绕过或降低安全审核。
|
||
- 不使用会关闭 Gemini 安全拦截的配置;如果 Gemini 安全机制拦截 prompt 或候选结果,该内容按审核不通过处理。
|
||
- OpenAI-compatible 转发模式下仍必须使用独立系统指令和结构化 JSON 解析;模型未返回明确合法结果时按审核失败处理。
|
||
- 模型返回格式不合法、网络失败、超时或限流失败时,内容标记为审核失败,不得公开。
|
||
- 只有 `approved` 状态可向普通访客公开;`unreviewed`、`reviewing`、`rejected`、`failed` 均不可公开。
|
||
- 审核不通过或审核失败时,后端可保存并通过 API / WebSocket 返回面向用户的简短原因详情;原因详情必须经过清洗和长度限制,不得包含 AI prompt、模型原始响应、内部错误、错误堆栈、调试信息、API Key、token/hash、系统策略原文或用户不需要处理的实现细节。
|
||
- 审核语言区独立于系统 UI 语言:
|
||
- 前台可选择 All languages 或具体语言区浏览内容。
|
||
- 发布时客户端可传当前语言区作为 hint,但最终语言区由服务端 AI 审核结果决定。
|
||
- 如果 AI 无法识别到启用语言区,回退到默认语言。
|
||
- 审核状态对普通访客不用于解释内部流程;只在作者本人或有管理权限的用户需要处理内容时展示。
|
||
|
||
## 全局配置数据
|
||
|
||
以下配置项都支持创建、编辑、删除、翻译和拖拽排序。物品分类、物品用途和 Ancient Artifacts 分类是代码维护的系统固定列表,不属于可配置数据。
|
||
|
||
### 特长
|
||
|
||
- 名称
|
||
- 是否有掉落物:`has_item_drop`
|
||
- 是否支持 Trading:`has_trading`
|
||
- 已移除 `subcategory` 字段。
|
||
- 当特长允许掉落物时,Pokemon 编辑中可为该 Pokemon + 特长配置一个掉落物品。
|
||
- 当 Pokemon 选择了至少一个支持 Trading 的特长时,Pokemon 详情页可直接维护该 Pokemon 对物品的 Trading 偏好观察。
|
||
|
||
### Pokemon Types
|
||
|
||
- 名称
|
||
- 用于 Pokemon 属性配置。
|
||
- Pokemon 可选择 1 到 2 个 Type,用于表达双属性。
|
||
|
||
### 喜欢的环境
|
||
|
||
- 名称
|
||
- Description:可为空,用于解释该 Ideal Habitat 对 Pokemon 栖息地选择的意义
|
||
- Opposite:可为空,双向关联另一个喜欢的环境作为反义关系;每个喜欢的环境最多只能属于一组 Opposite 配对;设置、替换或清空一侧时,系统必须在同一事务中同步维护另一侧
|
||
|
||
### 喜欢的东西 / 标签
|
||
|
||
- 名称
|
||
- Opposite:可为空,双向关联另一个喜欢的东西 / 标签作为反义关系;每个喜欢的东西 / 标签最多只能属于一组 Opposite 配对;设置、替换或清空一侧时,系统必须在同一事务中同步维护另一侧
|
||
- 同时用于:
|
||
- Pokemon 喜欢的东西
|
||
- 物品标签
|
||
|
||
### 入手方式
|
||
|
||
- 名称
|
||
- 可关联到物品和材料单。
|
||
|
||
### 地图
|
||
|
||
- 名称
|
||
- 用于栖息地中 Pokemon 出现地点。
|
||
|
||
### Game Version
|
||
|
||
- 版本号 / 名称
|
||
- ChangeLog:可为空,用于说明该版本主要变化。
|
||
- 用于 Life Post 发布时选择关联的游戏版本。
|
||
- Life Post 可不选择游戏版本;未选择时前台不展示版本号。
|
||
- Game Version 支持管理端创建、编辑、删除和排序。
|
||
|
||
## Pokemon
|
||
|
||
Pokemon 可配置:
|
||
|
||
- 内部 ID:`id`,系统唯一,用于路由、外键和实体关联;所有关联官方 data 的 Pokemon(包含普通 Pokemon 和 Event Pokemon)使用官方 data Pokemon ID 作为内部 ID;未关联官方 data 的自定义 Pokemon 由系统分配唯一内部 ID
|
||
- 官方 data 身份:`data_id` 和 `data_identifier`,可为空;用于记录该 Pokemon 对应的 CSV 官方 Pokemon ID 与 identifier,不作为用户可编辑展示 ID
|
||
- Pokopia 展示 ID:`display_id`,详情页、列表卡片和选择器中显示为 `#ID`,由 Pokopia 业务单独维护,不作为路由、外键或官方 data 身份
|
||
- 是否为 Event Pokemon:`is_event_item`
|
||
- 名称
|
||
- Genus:可为空,支持翻译
|
||
- 介绍 / Details:可为空,支持翻译
|
||
- Height:默认输入 `ft/in`,可切换输入 `m`;详情页同时展示 `ft/in` 与 `m`
|
||
- Weight:默认输入磅 `lb`,可切换输入 `kg`;详情页同时展示 `lbs` 与 `kg`
|
||
- Height / Weight 换算结果四舍五入;`m` / `kg` 保留 2 位小数,`in` 取整数,`lb` 保留 1 位小数。
|
||
- Types:可多选,最多 2 个
|
||
- 喜欢的环境:单选
|
||
- 特长:可多选,最多 2 个
|
||
- 特长掉落物品:按 Pokemon + 特长配置,单选物品
|
||
- 喜欢的东西:可多选,最多 6 个
|
||
- Trading:由所选特长是否支持 Trading 决定;当至少一个所选特长支持 Trading 时,可维护该 Pokemon 对物品的 Trading 偏好观察,分为 Likes 与 Neutral
|
||
- Likes:该 Pokemon 喜欢交易该物品,交易价格触发 1.5x 加成;用于物品隐藏标签推断的正向证据
|
||
- Neutral:该 Pokemon 对交易该物品无加成;用于物品隐藏标签推断的硬排除证据
|
||
- 每个物品在同一个 Pokemon 的 Trading 列表中只能出现一次,只能属于 Likes 或 Neutral 其中一组
|
||
- 六维:
|
||
- HP
|
||
- Attack
|
||
- Defense
|
||
- Special Attack
|
||
- Special Defense
|
||
- Speed
|
||
- 出现的栖息地:由栖息地出现配置反向展示
|
||
- 翻译
|
||
|
||
普通 Pokemon 与 Event Pokemon 分开展示:
|
||
|
||
- `/pokemon` 展示普通 Pokemon 列表。
|
||
- `/event-pokemon` 展示 Event Pokemon 列表。
|
||
- 两个列表复用 Pokemon 筛选、卡片和详情行为,但列表请求必须按 `is_event_item` 分开读取。
|
||
|
||
Pokemon 的 Pokopia 展示 ID 在普通 Pokemon 和 Event Pokemon 之间可以重复,例如允许同时存在普通 `#1 妙蛙种子` 和 Event `#1 毽子草`。数据库只要求同一个 `display_id + is_event_item` 组合唯一;前端路由和实体关联必须继续使用内部 `id`,不能使用展示 ID 作为路由或外键。Fetch 得到的官方 data ID 必须与展示 ID 分开保存;例如 Zorua 的官方 data ID 为 `570` 时,用户把 Pokopia 展示 ID 改成 `123` 后仍应通过 `/pokemon/570` 访问该 Pokemon,`/pokemon/123` 只代表内部 ID 为 `123` 的其他 Pokemon。普通 Pokemon 和 Event Pokemon 不会同时存在同一个内部系统 ID;当 Event Pokemon 关联官方 data 时,内部 ID 同样使用官方 data Pokemon ID。
|
||
|
||
Pokemon 编辑表单使用标签页组织字段:
|
||
|
||
- 编辑表单提供 Fetch data 功能:
|
||
- 已验证且拥有 `pokemon.fetch` 权限的用户可在 Fetch 输入框输入 data identifier 或官方 data Pokemon ID,从同一个搜索输入查询基础资料或图片候选。
|
||
- Fetch data 从仓库 `data/` CSV 查询基础资料并填入当前表单。
|
||
- Fetch 输入框提供 data 列表搜索,搜索范围包含 Pokemon ID、identifier、当前语言名称和默认语言名称;结果只展示 `#ID`、名称和 identifier。
|
||
- Fetch 搜索结果默认关闭,只在用户主动点击输入框或输入内容时展开;Escape、失焦 / 点击外部、选择结果后关闭。
|
||
- Fetch 搜索不使用防抖或节流;前端在每次新搜索时取消上一条搜索请求,并且只渲染最新请求结果。
|
||
- Fetch 只填入 CSV 可提供的字段:官方 data ID、官方 data identifier、名称、Genus、Height、Weight、Types、六维和名称/Genus 翻译;不填入 Details、喜欢的环境、特长、特长掉落物品或喜欢的东西。
|
||
- Fetch data 不要求官方 data ID 与 Pokopia 展示 ID 相同;若表单 ID 已有用户输入则保留该展示 ID,只有新建且 ID 为空时才用官方 data ID 作为初始展示 ID。
|
||
- Fetch 后保存关联官方 data 的 Pokemon 时,官方 data ID 作为内部路由 ID;Pokopia 展示 ID 只保存到 `display_id`。
|
||
- Fetch 不直接创建或更新 Pokemon;用户仍需通过 Save 保存,保存时沿用现有编辑审计。
|
||
- Fetch 根据 `languages.code` 自动匹配 CSV 语言列:`en`、`ja`、`ko`、`fr`、`de`、`es`、`it` 使用同名列;`zh-CN` / `zh-SG` 等简体语言使用 `zh_hans`;`zh-TW` / `zh-HK` / `zh-MO` 使用 `zh_hant`。
|
||
- Fetch 会自动确保 canonical Pokemon Types 存在于 `pokemon_types`,Type ID 与 `data/localized_type_name.csv` 和 `frontend/public/types` 图标文件保持一致;用户不需要为 Fetch 手工创建 Type 配置。
|
||
- Type 展示使用 `frontend/public/types/small/{typeId}.png` 图标并保留文字名称。
|
||
- 编辑表单提供 Pokemon 图片选择功能:
|
||
- 已验证且拥有 `pokemon.fetch` 权限的用户通过 Fetch data 的同一个 data identifier / 官方 data Pokemon ID 输入框,从 `https://pokesprite.tootaio.com/sprites/` 静态图片树查询对应 Pokemon 的可用图片候选。
|
||
- 图片候选只使用 `/sprites/pokemon/...` 相对路径,后端按固定资源族生成候选并用 `HEAD` 校验存在性;不保存任意外部 URL。
|
||
- 静态图片与官方 data identifier / 官方 data Pokemon ID 关联,不与 Pokopia 可编辑展示 ID 关联;用户修改 Pokopia 展示 ID 后,已选静态图片仍可保存。
|
||
- 图片选择不直接创建或更新 Pokemon;用户仍需通过 Save 保存,保存时沿用现有编辑审计。
|
||
- 图片选择界面使用 Pokédex 风格:上方显示当前选择的大图,大图下方显示版本、状态和描述,再下方以缩略图网格展示同一 Pokemon 的不同风格 / 版本 / 状态。
|
||
- Pokemon 保存显示图片的相对路径、风格、版本、状态和描述;API 对外返回可直接展示的图片 URL,但不暴露内部校验状态。
|
||
- Pokemon 也支持社区上传图片;上传图片使用通用 Wiki 图片上传历史,当前显示图片可在静态候选和上传图片之间切换。
|
||
- 基础标签页:
|
||
- 第一行:Pokopia 展示 ID、名称
|
||
- 第二行:喜欢的环境、特长
|
||
- 第三行:喜欢的东西
|
||
- 特长掉落物品随已选择且支持掉落物的特长显示
|
||
- 编辑表单不直接维护 Trading 观察;Trading 由详情页的 Manage Trading 入口维护
|
||
- Pokemon 图片选择区
|
||
- Advance 标签页:
|
||
- 第一行:Genus
|
||
- 第二行:Details
|
||
- 第三行:Height / Weight,身高与体重控件在桌面端同一行展示
|
||
- 第四行:Types
|
||
- 第五行:六维 Stats
|
||
|
||
Pokemon 列表功能:
|
||
|
||
- 搜索
|
||
- 按喜欢的环境筛选
|
||
- 按特长筛选:
|
||
- 满足任意条件
|
||
- 满足全部条件
|
||
- 按喜欢的东西筛选:
|
||
- 满足任意条件
|
||
- 满足全部条件
|
||
- 按 Pokopia 展示 ID(`display_id`)升序展示;内部 `id` 仅用于路由、外键和稳定排序兜底
|
||
- 列表首屏只读取一页数据;滚动到列表底部时继续读取下一页,不一次性加载全部 Pokemon。
|
||
- Pokemon 列表卡片只展示 Pokemon 图片和下方的 `#ID 名称`;不展示喜欢的环境、属性、特长、喜欢的东西或编辑元信息。
|
||
- Pokemon 卡片在已配置图片时展示所选图片缩略图;未配置图片时保留默认 Poké Ball 标记。
|
||
- Event Pokemon 列表功能与 Pokemon 列表相同,但只展示 `is_event_item = true` 的 Pokemon;Pokemon 列表只展示 `is_event_item = false` 的 Pokemon。
|
||
|
||
Pokemon 详情页展示:
|
||
|
||
- 基本信息
|
||
- 标题区不展示 Ideal Habitat;Ideal Habitat 属于正文核心资料。
|
||
- 详情主内容顶部改为左侧 Pokemon 图片、右侧 Pokemon Description;已配置图片时展示居中的 Pokédex 风格图片,未配置图片时展示默认 Poké Ball 占位符;页面内不直接展示图片版本、状态或描述,用户可通过图片 Modal 查看详情。
|
||
- 详情页需要突出 Pokopia 机制核心要素:
|
||
- Skills:影响栖息地选择、物品掉落和 Trading 行为
|
||
- Ideal Habitat:影响栖息地选择和相关 Pokemon 对比;正文中只展示正向 Ideal Habitat 名称和 Description,不展示反义词
|
||
- Favourite Things:影响物品掉落、隐藏标签判断和 Trading 价格证据;正文中只展示正向 Favourite Things,不展示反义词
|
||
- 特长掉落物品:当该 Pokemon 拥有支持掉落物的已选特长时默认展示;已配置时展示掉落物品图标,未配置时展示空状态
|
||
- Trading:当该 Pokemon 拥有支持 Trading 的已选特长时默认展示;分 Likes 与 Neutral 两组展示物品,Likes 表示交易价格 1.5x,Neutral 表示无加成,未配置观察时展示空状态;详情页双列列表高度受限,内容较多时每列内部独立滚动,避免 Section 被大量物品无限拉长
|
||
- Trading 可在详情页通过 Manage Trading Modal 维护;Modal 左侧顶部为同一行搜索框和分类下拉筛选,下面提供 Likes / Neutral 默认加入目标切换;搜索结果优先展示名称精确匹配、名称开头匹配和单词匹配的物品,再展示名称包含、分类或用途包含的物品;搜索框支持上下键切换当前结果、左右键切换默认加入组、Enter 将当前结果加入默认目标组;从左侧选择物品会加入当前默认目标组,右侧按 Likes / Neutral 分组展示已选物品,并可切换分组或移除;列表区域高度稳定,过滤结果减少时 Modal 不应抖动
|
||
- 参考资料 Tab:Height / Weight、Types 和六维 Stats 移到独立 Tab;该 Tab 必须注明这些数据只是参考 Pokédex 的展示设计,不属于 Pokopia 机制;六维使用 ProgressBar 展示,最大值按 150 计算
|
||
- 相关 Pokemon:与关联喜欢的东西的物品在桌面端左右并排展示;按相同喜欢的环境优先,其次按共同喜欢的东西数量从多到少排序;支持按喜欢的环境筛选,默认筛选当前 Pokemon 的喜欢的环境,也可切换到其他喜欢的环境或全部;每个筛选视图最多展示 6 个;每项左侧展示 Pokemon 图片或默认 Poké Ball 占位符,原列表项信息布局保持不变,第一行左侧展示名称,右侧展示特长和喜欢的环境,第二行展示喜欢的东西,并高亮共同喜欢的东西;当相关 Pokemon 的 Ideal Habitat 或 Favourite Things 与当前 Pokemon 配置的 Opposite 反义关系命中时,只使用红色标记,不展示反义词或额外 Opposite 文案
|
||
- 关联喜欢的东西的物品:与相关 Pokemon 在桌面端左右并排展示;每项展示物品图标,未配置图标时显示默认物品标记占位符
|
||
- 出现的栖息地:每行左侧展示栖息地图片,未配置图片时显示默认栖息地标记占位符
|
||
- 最后编辑信息
|
||
- 讨论
|
||
- 编辑历史:通过详情页 Tabs 展示
|
||
|
||
## 物品
|
||
|
||
物品可配置:
|
||
|
||
- 名称
|
||
- 介绍
|
||
- Base Price:可为空
|
||
- Ancient Artifact:可为空,Items Edit 使用单选框维护;`No` 表示普通物品,其他值使用系统固定列表:
|
||
- Lost Relics (L)
|
||
- Lost Relics (S)
|
||
- Fossils
|
||
- 是否为 Event Item:`is_event_item`
|
||
- 分类:必填,使用系统固定列表,不在管理端配置:
|
||
- Furniture
|
||
- Misc
|
||
- Outdoor
|
||
- Utilities
|
||
- Buildings
|
||
- Blocks
|
||
- Kits
|
||
- Nature
|
||
- Food
|
||
- Materials
|
||
- Key Items
|
||
- Other
|
||
- 用途:可为空,使用系统固定列表,不在管理端配置:
|
||
- Decoration
|
||
- Relaxation
|
||
- Toy
|
||
- Road
|
||
- 入手方式:可多选
|
||
- 客制化:
|
||
- 染色能力:`dyeability`,使用互斥枚举值维护:
|
||
- `0`:不可染色
|
||
- `1`:可染色
|
||
- `2`:可双区染色
|
||
- `3`:可三区染色
|
||
- 可改花纹
|
||
- 无材料单:`no_recipe`
|
||
- 标签:使用喜欢的东西配置,可多选
|
||
- 图标图片:通过通用 Wiki 图片上传维护当前图标和历史上传记录
|
||
- Data Tools 的 Items CSV 导入可为物品写入静态图标路径 `/pokopia/items/{image_file_name}`;静态图标展示 URL 为 `https://pokesprite.tootaio.com/pokopia/items/{image_file_name}`,用户后续仍可在编辑页切换为社区上传图片
|
||
- 翻译
|
||
- 排序
|
||
|
||
Items 与 Event Items 使用相同数据模型:
|
||
|
||
- Items 列表只展示 `is_event_item = false` 的物品。
|
||
- Event Items 列表只展示 `is_event_item = true` 的物品。
|
||
- Event Items 与 Items 共用物品分类、用途、入手方式、标签、图片、翻译和材料单逻辑。
|
||
- 已选择 Ancient Artifact 分类的物品仍显示在 Items / Event Items 列表中,并额外进入 Ancient Artifacts 对应分类列表。
|
||
|
||
物品列表功能:
|
||
|
||
- 搜索
|
||
- 按分类展示为标签页
|
||
- 按用途筛选
|
||
- 按标签筛选
|
||
- 按自定义排序展示
|
||
- 公开列表首屏只读取一页数据;滚动到列表底部时继续读取下一页,不一次性加载全部 Items 或 Event Items。
|
||
- All 视图在满足写入权限时支持对 Grid Item 右键插入新物品到前/后,并支持直接拖曳 Item 调整排序;插入与拖曳只作用于当前展示的 Items 列表,不影响 Event Items 入口。
|
||
- 新增物品入口支持当前浏览器 Session 的默认值菜单;用户可为新建物品预设分类、用途、客制化勾选项和入手方式。默认值只影响 `/items/new` 与 `/event-items/new` 的新建表单初始值,不影响编辑已有物品,不改变 API、数据库模型、权限或审计行为;Event Items 仍由 `/event-items/new` 入口决定 `is_event_item`。
|
||
- 物品列表桌面端使用 12 列紧凑 Grid,每个格子只展示物品图标;有用途的物品在卡片左上角以斜 Ribbon 展示用途名称;物品名称通过 hover / focus Tooltip 展示。
|
||
- 物品列表移动端保持常规卡片布局,展示物品图标、名称和分类。
|
||
- 物品列表不展示标签、入手方式或编辑元信息。
|
||
- 已配置图标时,物品卡片展示图标缩略图;未配置图标时保留默认物品标记。
|
||
|
||
物品详情页展示:
|
||
|
||
- 基本信息
|
||
- 当前图标图片;未配置图标时展示默认物品标记占位符
|
||
- 顶部按图标 / 占位符与核心信息概览并排展示,移动端改为单列;顶部概览卡片不显示 `Image` / `Details` 通用区块标题,也不展示图片历史缩略图
|
||
- 介绍
|
||
- Base Price
|
||
- Ancient Artifact 分类:仅在物品已配置 Ancient Artifact 分类时展示
|
||
- 分类
|
||
- 用途
|
||
- 入手方式
|
||
- 客制化
|
||
- 标签
|
||
- Possible Tags:根据所有拥有支持 Trading 特长的 Pokemon Trading 观察推断该物品可能包含的隐藏标签
|
||
- 每个 Pokemon 的“喜欢的东西”视为该 Pokemon 已知的 6 个隐藏标签集合;不完整数据仍参与展示,但不会强行补足缺失标签
|
||
- 若物品被 Pokemon 标记为 Likes,则该物品至少包含该 Pokemon 标签集合中的一个标签,属于 OR 正向证据
|
||
- 若物品被 Pokemon 标记为 Neutral,则该物品不包含该 Pokemon 标签集合中的任何标签,属于硬排除证据;Neutral 排除优先于 Likes 正向证据
|
||
- 推断流程必须确定性执行:从所有“喜欢的东西 / 标签”开始,先移除所有 Neutral Pokemon 提供的标签,再用 Likes Pokemon 的标签集合收窄候选;多个 Likes 观察的共同候选归为 Highly likely,其余正向候选归为 Possible,被排除或被约束移出的标签归为 Excluded
|
||
- 没有可用 Likes 观察时,未被 Neutral 排除的标签保持 Possible;没有任何观察时,所有标签保持 Possible
|
||
- Possible Tags 区块必须展示 Likes 与 Neutral 证据来源,包含贡献 Pokemon 及其已知标签,不展示内部字段、调试信息或推断中间状态
|
||
- 关联材料单:展示结果物品图标和材料物品图标;未配置图标时显示默认物品标记占位符
|
||
- 作为材料出现的材料单:展示结果物品图标和材料物品图标;未配置图标时显示默认物品标记占位符
|
||
- 相关栖息地:展示栖息地图片或默认栖息地标记占位符,并展示配方材料物品图标
|
||
- 相关 Pokemon 掉落:展示 Pokemon 图片;未配置图片时显示默认 Poké Ball 占位符
|
||
- 最后编辑信息
|
||
- 讨论
|
||
- 编辑历史
|
||
|
||
## Ancient Artifacts
|
||
|
||
Ancient Artifacts 是 Items 的可选分类视图,不再维护独立主数据结构或独立表;列表、详情和排序从 `items.ancient_artifact_category_key IS NOT NULL` 的物品获取。已配置 Ancient Artifact 分类的物品仍保留在 Items / Event Items 列表中,并额外出现在 Ancient Artifacts 对应分类列表。Ancient Artifact 路由继续保留,用于浏览、编辑和导航对应的物品记录。
|
||
|
||
- 名称
|
||
- 介绍
|
||
- 图片:使用 Items 编辑器和上传目录,支持图片历史
|
||
- 分类:在 Items Edit 的 Ancient Artifact 单选框中维护;`No` 表示不进入 Ancient Artifacts 列表,其他选项使用系统固定列表,不在管理端配置:
|
||
- Lost Relics (L)
|
||
- Lost Relics (S)
|
||
- Fossils
|
||
- 标签:复用全局“喜欢的东西 / 标签”配置,可多选
|
||
- 翻译
|
||
- 排序
|
||
|
||
Ancient Artifacts 列表功能:
|
||
|
||
- 搜索
|
||
- 按分类展示为标签页
|
||
- 按标签筛选
|
||
- 按自定义排序展示
|
||
- 列表桌面端使用 12 列紧凑 Grid,每个格子只展示图片 / 默认 Ancient Artifact 标记;名称通过 hover / focus Tooltip 展示。
|
||
- 列表移动端保持常规卡片布局,展示图片 / 默认 Ancient Artifact 标记、名称和分类。
|
||
- 列表不展示编辑元信息。
|
||
|
||
Ancient Artifacts 详情页使用同一套 Item Details 视图展示同一条 `items` 记录;顶部、图片、基础信息、Base Price、物品分类、用途、入手方式、客制化、标签、材料单关联、讨论和编辑历史均按物品详情页规则展示,并额外展示 Ancient Artifact 分类。通过 `/ancient-artifacts/:id` 打开的普通非 Ancient Artifact 物品会回到对应 `/items/:id`。
|
||
|
||
## 材料单
|
||
|
||
材料单与物品是一对一关系:
|
||
|
||
- 一个材料单必须关联一个结果物品。
|
||
- 一个物品最多只能有一个材料单。
|
||
- 标记为 `no_recipe` 的物品不能创建材料单。
|
||
- 材料单没有独立名称,展示名称来自结果物品。
|
||
|
||
材料单可配置:
|
||
|
||
- 结果物品
|
||
- 入手方式:可多选
|
||
- 需要材料:多项物品 + 数量
|
||
- 排序
|
||
|
||
材料单列表功能:
|
||
|
||
- 独立于物品列表展示
|
||
- 按结果物品分类展示
|
||
- 按自定义排序展示
|
||
- 材料单列表卡片使用与 Pokemon 列表一致的居中图鉴式布局,按结果物品展示图标、名称和分类;不展示编辑元信息。
|
||
- 有用途的结果物品在卡片左上角以斜 Ribbon 展示用途名称。
|
||
- Create Recipe 按钮展示在结果物品名称下方;已有材料单的卡片保留同等按钮空间但不显示按钮;标记为无材料单的物品展示禁用按钮;可创建材料单的物品展示可点击按钮并进入创建流程。
|
||
|
||
材料单详情页展示:
|
||
|
||
- 结果物品图片或默认材料单标记占位符;顶部概览卡片不显示 `Image` / `Details` 通用区块标题
|
||
- 结果物品名称、分类和用途;`GET /api/recipes/:id` 的 `item` 字段返回展示所需的 `id`、`name`、`image`、`category`、`usage`
|
||
- 入手方式
|
||
- 需要材料列表:展示材料物品图标;未配置图标时显示默认物品标记占位符
|
||
- 最后编辑信息
|
||
- 讨论
|
||
- 编辑历史
|
||
|
||
## Dish
|
||
|
||
Dish 是公开浏览的料理资料入口,按可配置分类组织。
|
||
|
||
Dish Category 可配置:
|
||
|
||
- 名称
|
||
- 厨具:关联 Items
|
||
- 主材料:关联 Items,必填
|
||
- 吃了之后的效果
|
||
- 总数所需材料数量:最小值为 2
|
||
- 翻译
|
||
- 排序
|
||
|
||
Dish 可配置:
|
||
|
||
- 所属 Dish Category
|
||
- 菜肴:关联 Items
|
||
- 味道:使用 System Config 中可配置的 Dish Flavor
|
||
- 副材料:关联 Items,可选
|
||
- 第二副材料:关联 Items,仅当所属分类的总数所需材料数量大于 2 时可配置
|
||
- Pokemon 特征:可选,复用现有特长配置
|
||
- 给苔藓卡比兽(Mosslax)吃之后的效果
|
||
- 翻译
|
||
- 排序
|
||
|
||
Dish 页面功能:
|
||
|
||
- `/dish` 是公开浏览入口。
|
||
- 分类使用 Tabs 展示。
|
||
- `/dish` 可直接添加、编辑和删除 Dish Category 与 Dish;写入入口按 `dish.*` 权限展示,后端仍做权限校验。
|
||
- 每个分类第一行展示分类名、厨具、主材料和总数所需材料数量;第二行展示吃后效果。
|
||
- 每个菜肴展示菜肴物品、味道、可选副材料、可选第二副材料、可选 Pokemon 特征和 Mosslax 效果。
|
||
- Item、特长和 Dish Flavor 名称按当前语言解析;Dish Category 名称、吃后效果和 Dish Mosslax 效果按当前语言解析。
|
||
- Dish 公开 API 只返回浏览需要的 Item、特长、材料、效果和审计字段,不返回内部字段、权限、token/hash 或调试信息。
|
||
- Dish 分类和菜肴的创建、更新、删除、排序必须记录编辑历史和编辑者信息。
|
||
|
||
## 栖息地
|
||
|
||
栖息地可配置:
|
||
|
||
- 名称
|
||
- 是否为活动物品:`is_event_item`
|
||
- 配方:多项物品 + 数量
|
||
- 可出现的 Pokemon
|
||
- 图片:通过通用 Wiki 图片上传维护当前图片和历史上传记录
|
||
- 翻译
|
||
- 排序
|
||
|
||
Pokemon 出现配置:
|
||
|
||
- Pokemon
|
||
- 地图:可多选
|
||
- 时间:可多选
|
||
- 早晨
|
||
- 中午
|
||
- 傍晚
|
||
- 晚上
|
||
- 天气:可多选
|
||
- 晴天
|
||
- 阴天
|
||
- 雨天
|
||
- 稀有度:1 到 3 星
|
||
|
||
栖息地列表功能:
|
||
|
||
- 按自定义排序展示
|
||
- 栖息地列表卡片使用与 Pokemon 列表一致的居中图鉴式布局,只展示栖息地图片和名称;不展示配方摘要、可能出现的 Pokemon 摘要或编辑元信息。
|
||
- 已配置图片时,栖息地卡片展示图片缩略图;未配置图片时保留默认栖息地标记。
|
||
- `/habitats` 只展示 `is_event_item = false` 的普通栖息地。
|
||
- `/event-habitats` 只展示 `is_event_item = true` 的 Event Habitats。
|
||
- Event Habitats 列表复用栖息地列表的排序、卡片和详情行为;详情、编辑、关联和讨论继续使用内部 `id`。
|
||
|
||
栖息地详情页展示:
|
||
|
||
- 当前图片;未配置图片时展示默认栖息地标记占位符
|
||
- 顶部按图片 / 占位符与核心信息概览并排展示,移动端改为单列;顶部概览卡片不显示 `Image` / `Details` 通用区块标题,也不展示图片历史缩略图
|
||
- 配方列表:展示材料物品图标;未配置图标时显示默认物品标记占位符
|
||
- 可能出现的 Pokemon 列表:展示 Pokemon 图片;未配置图片时显示默认 Poké Ball 占位符
|
||
- 出现时间
|
||
- 出现天气
|
||
- 稀有度
|
||
- 出现的地图列表
|
||
- 最后编辑信息
|
||
- 讨论
|
||
- 编辑历史
|
||
|
||
## 每日 CheckList
|
||
|
||
每日 CheckList Task 可配置:
|
||
|
||
- Task 标题
|
||
- 翻译
|
||
- Task 顺序
|
||
|
||
前台行为:
|
||
|
||
- 展示每日要做的 Task。
|
||
- 每个 Task 可勾选。
|
||
- 勾选状态保存在浏览器本地。
|
||
- 勾选状态按本地日期自动清空,不删除 Task。
|
||
- 已删除 Task 的本地勾选状态会自动清理。
|
||
|
||
管理行为:
|
||
|
||
- 已验证且拥有对应 CheckList 权限的用户可新增、编辑、删除 Task。
|
||
- 已验证且拥有 `checklist.order` 权限的用户可通过 Handle 拖拽排序。
|
||
|
||
## Life
|
||
|
||
Life 是社区生活分享信息流,类似轻量社交动态。
|
||
|
||
Life Post 可配置:
|
||
|
||
- Post 内容正文
|
||
- Game Version:可为空,使用 Game Version 配置;有值时在 Post 卡片展示版本号。
|
||
- 创建者、最后编辑者、创建时间、最后编辑时间
|
||
- 评论
|
||
- 评论回复:仅支持回复顶层评论,不做无限嵌套
|
||
- Reactions:`like`、`helpful`、`fun`、`thanks`
|
||
|
||
前台行为:
|
||
|
||
- 所有人都可以浏览 Life 信息流。
|
||
- 信息流按创建时间倒序展示。
|
||
- Life Post 有独立详情页 `/life/:id`;用户可从 Life 信息流、User Profile 的 Feeds、Reactions 和 Comments 进入。
|
||
- 已注册并完成邮箱验证且拥有 `life.posts.create` 权限的用户可以发布 Life Post。
|
||
- 作者本人拥有 `life.posts.update` / `life.posts.delete` 权限时可以编辑、删除自己的 Life Post;删除 Life Post 使用软删除。
|
||
- 拥有 `life.posts.update-any` / `life.posts.delete-any` 权限的用户可以管理其他用户的 Life Post。
|
||
- 已注册并完成邮箱验证且拥有 `life.comments.create` 权限的用户可以评论 Life Post,并回复顶层评论。
|
||
- 评论作者拥有 `life.comments.delete` 权限时可以删除自己的评论;拥有 `life.comments.delete-any` 权限的用户可以删除其他用户评论;删除后的 Life Comment 仅对该评论作者本人可见并保留正文,作者可通过 Undo 恢复;其他用户不可见,不显示 Deleted Comment 占位,不出现在评论列表、评论预览或评论数量中。
|
||
- 已软删除的 Life Post 不出现在信息流或搜索结果中,也不能继续编辑、评论或设置 Reaction。
|
||
- 已软删除的 Life Post 详情页返回未找到,不公开软删除字段。
|
||
- 每条 Life Post 默认只展示评论入口与评论数量;评论列表、回复和评论输入默认折叠,用户点击后展开。
|
||
- Life Post 详情页默认展示该 Post 的评论区,并使用独立分页接口继续加载完整评论列表。
|
||
- Life Feed 只随每条 Life Post 返回评论总数和最近少量评论预览;完整评论列表在展开评论区后通过独立分页接口读取,每页顶层评论携带其一层回复。
|
||
- Life Comment 列表支持 `sort`:`oldest` 默认按创建时间正序;`latest` 按创建时间倒序;`most-liked` 按点赞数倒序;`most-replied` 按直接回复数倒序;同分时使用创建时间和 ID 保持稳定排序。排序只作用于顶层评论,回复始终按创建时间正序展示。
|
||
- 已注册并完成邮箱验证且拥有 `life.comments.like` 权限的用户可以点赞或取消点赞审核通过且未删除的 Life Comment;每个用户对每条评论最多 1 个 Like。
|
||
- 已注册并完成邮箱验证且拥有 `life.reactions.set` 权限的用户可以对每条 Life Post 选择一个 Reaction;普通点击默认设置 `like`,再次点击 `like` 会取消,当前为其他 Reaction 时普通点击会替换为 `like`。
|
||
- Life Reaction 的其他类型通过右键 / context menu 或可见展开按钮打开 Popup 选择;再次选择当前 Reaction 会取消,选择其他 Reaction 会替换原 Reaction。
|
||
- 用户可在 Life Post 的 Reaction 汇总处打开 Modal 查看公开 Reaction 用户列表;列表支持按 Reaction 类型筛选并分页加载。
|
||
- 支持按 Life Post 正文搜索;用户按 Enter 或点击 Search 按钮后提交搜索,不随输入实时请求;搜索结果仍按创建时间倒序展示并分页加载。
|
||
- Feed 使用语言筛选展示 All languages 和启用语言;语言区筛选独立于系统 UI 语言,搜索和语言筛选可以同时生效。
|
||
- Feed 支持按 Game Version 筛选;All versions 表示不过滤版本。
|
||
- Feed 支持排序:Latest 默认按创建时间倒序;Oldest 按创建时间正序。
|
||
- 登录用户可切换 All Feed 和 Following Feed;Following Feed 只展示当前用户已 Follow 用户发布且当前用户可见的 Life Post,并继续支持搜索、语言、Game Version 和排序筛选。
|
||
- 信息流分页加载,初始展示最新一页,滚动到底部自动加载更多。
|
||
- 当前没有图片上传、转发或置顶。
|
||
- Life Post 和 Life Comment 必须进入 AI 审核;未审核通过的内容不向普通访客公开。
|
||
- 作者本人和拥有对应 `*-any` 管理权限的用户可以看到相关内容的审核状态,并可触发重新审核。
|
||
- 未审核通过的 Life Post 详情只对作者本人和拥有对应管理权限的用户可见;普通访客访问时返回未找到。
|
||
- Life Post 必须展示未通过或未完成的审核状态:审核中、未审核、审核失败、审核不通过;审核通过不显示状态标签。
|
||
- 新增或更新 Life Post 后先进入不可公开状态,AI 审核通过后才出现在普通公开 Feed。
|
||
- Life Comment 和回复审核通过且未删除后才出现在普通公开评论列表、评论数量和评论预览中;已删除评论只在作者自己的可见评论列表、评论数量和评论预览中保留,以便作者 Undo。
|
||
- 审核失败不等于审核通过;失败内容保持不可公开,用户可重新审核。
|
||
- `reviewing` 表示审核正在进行中,前端不展示重新审核入口;只有 `unreviewed`、`rejected` 和 `failed` 这类非进行中且未通过状态可触发重新审核,API 也必须拒绝对 `reviewing` 或 `approved` 评论重新审核。
|
||
- Life Post 是用户生成内容,正文按作者输入展示,不进入 `entity_translations`。
|
||
|
||
API 暴露边界:
|
||
|
||
- Life Post 作者信息只返回 `id` 和 `displayName`。
|
||
- Life Post Game Version 只返回 `id`、展示用 `name` 和可展示 `changeLog`;未选择版本时返回 `null`。
|
||
- Life Post 可返回面向用户展示所需的审核状态、审核语言区、审核原因详情和是否可重审;审核原因详情仅用于 `rejected` / `failed`,不返回内部错误、AI prompt、模型响应、错误堆栈或 retry 细节。
|
||
- Life Comment 作者信息只返回 `id` 和 `displayName`。
|
||
- Life Comment 只返回 `likeCount`、`replyCount` 和当前用户自己的 `myLiked`;不返回点赞用户列表、邮箱、角色、权限或内部审计。
|
||
- Life Post 列表和详情中的 Life Reaction 只返回按类型汇总的数量和当前用户自己的 Reaction,不内嵌其他用户明细。
|
||
- Life Reaction 用户列表 API 只返回公开用户摘要 `id`、`displayName`、`reactionType` 和 `reactedAt`;不返回邮箱、角色、权限、token/hash、内部审计或其他用户隐私字段。
|
||
- Life Post 列表 API 返回分页结果:`items`、`nextCursor`、`hasMore`;`cursor` 是不透明分页令牌。每个 Life Post 的评论字段只包含已公开或当前用户可见评论的 `commentCount` 和 `commentPreview`,不内嵌完整评论列表。
|
||
- Life Post 详情 API 返回单条 Life Post,字段边界与列表项一致;评论字段仍只包含 `commentCount` 和少量 `commentPreview`,完整评论通过评论分页接口读取。
|
||
- Life Comment 列表 API 返回分页结果:`items`、`nextCursor`、`hasMore`、`total`;`cursor` 是不透明分页令牌;普通访客只读取审核通过评论;支持 `sort` 为 `oldest`、`latest`、`most-liked` 或 `most-replied`。
|
||
- Life Comment 可返回作者本人或管理用户处理评论所需的审核状态、语言区和 `rejected` / `failed` 原因详情。
|
||
- API 不返回邮箱、token/hash、内部调试字段、AI prompt、模型原始响应、内部审核错误、错误堆栈或不必要的审计 payload。
|
||
- API 不返回 Life Post 的 `deleted_at`、`deleted_by_user_id` 等内部软删除字段。
|
||
- 非作者只有拥有对应 `*-any` 管理权限时才能编辑或删除其他用户的 Life Post 或 Life Comment。
|
||
|
||
## Threads
|
||
|
||
Threads 是社区长期讨论区,形态类似 Discord Forum + 聊天室混合系统。
|
||
|
||
Channel 可配置:
|
||
|
||
- 名称
|
||
- 是否允许用户创建 Thread
|
||
- 可用标签:每个 Channel 内唯一,按 `sort_order` 展示
|
||
- 可用语言:使用 `languages.code`,可配置允许的语言集合;未配置时前台回退到启用语言
|
||
- `sort_order`
|
||
|
||
Thread 可配置:
|
||
|
||
- 标题
|
||
- 所属 Channel
|
||
- 标签:多选,只能选择该 Channel 可用标签
|
||
- 语言:只能选择该 Channel 可用语言或启用语言中的一项
|
||
- 创建者、创建时间、最后活跃时间
|
||
- 消息数
|
||
- Follow 状态
|
||
- Reaction 汇总
|
||
- 锁定状态
|
||
|
||
Message 可配置:
|
||
|
||
- 所属 Thread
|
||
- 正文
|
||
- 创建者、创建时间、更新时间
|
||
- Reaction 汇总
|
||
- AI 审核状态和语言区
|
||
|
||
前台行为:
|
||
|
||
- 所有人都可以浏览已公开的 Channel、Thread 和审核通过的 Message。
|
||
- `/threads` 展示 Threads 工作区,左侧为 Channel 列表,中间为 Thread List。
|
||
- `/threads/:threadId` 通过 route-backed Modal 打开 Thread 详情;默认进入最新消息位置。
|
||
- 用户可在 Channel 内创建 Thread;需要已注册、邮箱已验证并拥有 `threads.create` 权限,且 Channel 允许用户创建 Thread。
|
||
- 创建 Thread 时可从 Thread List 顶部搜索框预填 Title,Title 可在创建表单中继续修改。
|
||
- Thread 作者本人或拥有现有 Thread 管理权限的管理员可编辑 Thread 标题和 Tags;Tags 只能选择该 Channel 可用标签。
|
||
- 已注册、邮箱已验证并拥有 `threads.messages.create` 权限的用户可以在未锁定 Thread 中发送 Message。
|
||
- Thread Message 输入框中 Enter 发送,Ctrl + Enter 输入换行。
|
||
- Message 作者本人或拥有 `admin.threads.messages.delete` 权限的管理用户可编辑 Message 正文;编辑后 Message 重新进入 AI 审核,审核通过前不向普通访客公开。
|
||
- `unreviewed`、`rejected` 和 `failed` 状态的 Message 可由作者本人或拥有 `admin.threads.messages.delete` 权限的管理用户触发重新审核;`reviewing` 和 `approved` 状态不可重新审核。
|
||
- Message 列表按创建时间正序展示,新消息出现在底部。
|
||
- 初始读取最新一页 Message;向上滚动或点击 To Top 加载更早历史消息。
|
||
- 有新消息且用户不在底部时显示 Jump to Present;点击后滚动到最新消息。
|
||
- 连续 Message 在展示层自动合并:同一用户连续发送,且相邻消息时间间隔不超过 5 分钟;合并组只显示一次 Avatar、Username 和组首条 Timestamp。合并窗口默认 5 分钟,后续可由系统配置扩展。
|
||
- Thread 支持 Follow / Unfollow;Follow 后新审核通过 Message 会让 Threads Sidebar 和 Thread List 显示未读红点或未读提示。
|
||
- Thread 详情支持未读消息分隔线;用户进入最新位置或显式标记已读后更新 `thread_reads`。
|
||
- Thread 和 Message 支持 Emoji Reaction,当前提供默认快捷 Emoji:`👍`、`❤️`、`😂`、`🔥`、`👀`;API 只返回各类型数量和当前用户自己的 Reaction,不内嵌用户列表。
|
||
- Thread List 支持排序:`last-active` 默认按最后活跃倒序;`latest` 按创建时间倒序;`most-discussed` 按公开消息数倒序。
|
||
- Thread List 支持语言筛选:All languages 或指定启用语言 / Channel 可用语言。
|
||
- Thread List 支持按 Channel 标签筛选。
|
||
- Thread List 提供前端快速搜索,可在当前已加载列表内按 Thread 标题、作者展示名、语言和标签过滤;当前不提供后端全文搜索。
|
||
- Thread 新消息实时更新通过 Thread WebSocket;WebSocket 使用短期一次性 ticket,不把 session token 放入 WebSocket URL。
|
||
- Thread Message 是用户生成内容,必须经过 AI 审核;未审核通过的 Message 不向普通访客公开。作者本人和拥有 `admin.threads.messages.delete` 权限的管理用户可以看到自己的未通过/审核中 Message 状态。
|
||
- 审核通过的 Message 才计入普通公开消息数、最后活跃排序和未读状态。
|
||
- Thread 被锁定后不可新增 Message,但仍可浏览和设置 Reaction / Follow。
|
||
- 删除 Thread 使用软删除;删除后不出现在列表,详情返回未找到。
|
||
- 删除 Message 使用软删除;普通列表不展示已删除 Message,不暴露删除字段。
|
||
|
||
管理员行为:
|
||
|
||
- 拥有 `admin.threads.channels.read` 可查看 Channel 管理。
|
||
- 拥有 `admin.threads.channels.create` / `update` / `delete` 可创建、编辑、删除 Channel,配置标签、语言和是否允许用户创建 Thread。
|
||
- 拥有 `admin.threads.threads.delete` 可删除任意 Thread。
|
||
- 拥有 `admin.threads.threads.lock` 可锁定 / 解锁任意 Thread。
|
||
- 拥有 `admin.threads.messages.delete` 可删除任意 Message。
|
||
|
||
API 暴露边界:
|
||
|
||
- Channel API 只返回展示和管理需要的 `id`、`name`、`allowUserThreads`、`tags`、`languages`、`sortOrder` 和未读摘要;不返回内部审计或调试字段。
|
||
- Thread API 只返回 `id`、`channelId`、`title`、标签、语言、作者必要署名、创建时间、最后活跃时间、锁定状态、消息数、Reaction 汇总、当前用户 Reaction、Follow 状态和未读状态。
|
||
- Message API 只返回 `id`、`threadId`、`body`、作者必要署名、创建时间、更新时间、审核状态、语言区、必要审核原因、Reaction 汇总和当前用户 Reaction。
|
||
- API 不返回邮箱、角色、权限、session、token/hash、AI prompt、模型响应、内部审核错误、错误堆栈、删除时间、删除人或内部调试字段。
|
||
- Thread 内容正文按作者输入展示,不进入 `entity_translations`;Thread 的语言用于筛选和内容区分,不改变系统 UI 语言。
|
||
- Channel 名称和标签当前作为管理数据直接存储,不进入 `entity_translations`。
|
||
|
||
当前实现状态:
|
||
|
||
已实现:
|
||
|
||
- 数据库已包含 `thread_channels`、`thread_channel_tags`、`thread_channel_languages`、`threads`、`thread_tag_links`、`thread_messages`、`thread_reactions`、`thread_message_reactions`、`thread_follows`、`thread_reads` 和 `thread_ws_tickets`。
|
||
- 初始化会创建默认 Channel:General、Questions、Showcase。
|
||
- RBAC 已包含 Thread 用户权限:`threads.create`、`threads.messages.create`、`threads.follow`、`threads.reactions.set`。
|
||
- RBAC 已包含 Thread 管理权限:`admin.threads.channels.*`、`admin.threads.threads.delete`、`admin.threads.threads.lock`、`admin.threads.messages.delete`。
|
||
- 公开 API 已支持读取 Channel、分页读取 Thread、读取单个 Thread、读取 Thread Message 历史。
|
||
- 写入 API 已支持创建 Thread、发送与编辑 Message、Message 重新审核、Follow / Unfollow、标记已读、设置 / 取消 Thread Reaction、设置 / 取消 Message Reaction。
|
||
- 管理 API 已支持创建、编辑、删除 Channel,锁定 / 解锁 Thread,删除 Thread,删除 Message。
|
||
- Thread Message 已接入 AI 审核队列;审核通过后才更新 Thread 的公开 `message_count`、`last_message_id` 和 `last_active_at`。
|
||
- Thread WebSocket 已实现短期 ticket 连接,并可推送新审核通过 Message、Reaction 更新和当前用户 read 状态更新。
|
||
- 前端已新增 `/threads` 和 `/threads/:threadId`,包含 Channel Sidebar、Thread List、Thread 详情 Modal、创建 Thread、编辑 Thread 标题和 Tags、发送与编辑 Message、Message 重新审核、Follow / Unfollow、Reaction、管理员锁定 / 解锁 Thread、管理员删除 Thread 和管理员删除 Message。
|
||
- 前端 Message 展示已支持同一用户 5 分钟内连续消息的合并显示。
|
||
- 前端 Message 历史已支持点击 Load older 向上加载更早消息。
|
||
- 前端已支持 Jump to Present:用户不在底部且收到新消息时可跳到最新。
|
||
- 前端 Thread List 已支持 Channel、标签、语言和排序筛选。
|
||
- 前端管理端已新增 Thread Channels 管理入口,可配置 Channel 名称、是否允许用户创建 Thread、标签和语言。
|
||
|
||
未实现 / 待完善:
|
||
|
||
- Thread 详情中的未读消息分隔线尚未完整实现;当前已记录 read 状态并显示列表未读红点,但没有在消息流中定位并渲染 unread divider。
|
||
- WebSocket 没有自动重连、退避重试或跨标签页连接复用;连接断开后需页面重新加载或后续操作重新进入。
|
||
- Reaction 用户列表 Modal 尚未实现;当前只显示 Reaction 类型和数量,以及当前用户自己的 Reaction 状态。
|
||
- Thread / Message Reaction 取消 API 当前通过 JSON body 传入 `reactionType`,前端可用;若后续需要更标准的 REST 形态,可改为 `DELETE /reaction/:reactionType`。
|
||
- Channel 排序 UI 尚未实现;数据库已有 `sort_order`,但管理端目前不能拖拽或调整 Channel / Tag / Language 顺序。
|
||
- Channel 名称和标签尚未进入 `entity_translations`;当前按管理数据原文展示。
|
||
- Thread 创建后的首条 Message 如果审核失败,Thread 会存在但普通访客看不到公开 Message,前端尚未提供 Message 审核重试入口。
|
||
- Thread Message 审核失败 / 拒绝后的重试 API 和 UI 尚未实现。
|
||
- Thread 删除、Message 删除和锁定 / 解锁当前直接执行,尚未使用确认 Modal。
|
||
- Thread List 的实时排序更新是基础 upsert 行为;复杂筛选条件下收到不匹配当前筛选的新 Thread / Message 时,仍可能需要后续刷新来得到完全一致的列表。
|
||
- 移动端已使用响应式堆叠布局,但还不是独立的移动端双页导航体验;后续可优化为 Channel / Thread List / Chat 分步视图。
|
||
- 当前没有 Thread 后端全文搜索、置顶、收藏、编辑 Thread 语言、编辑 Message、上传图片、@mention 或通知到全局 NotificationBell。
|
||
|
||
## 开发中入口
|
||
|
||
以下前台公开入口当前仅展示“正在开发中”占位页,不提供数据模型、后端 API、编辑表单、管理入口或排序能力:
|
||
|
||
- Automation:未来用于分享自动化基地(亦称工厂)创建方案、材料产出、所需 Pokemon、生产顺序和共同喜好物品。
|
||
- Events
|
||
- Actions:游戏内快捷动作,例如挥手、跳舞等。
|
||
- Dream Island
|
||
- Clothes
|
||
|
||
这些开发中入口在主导航和占位页中显示状态 Badge,便于用户识别当前功能状态。
|
||
|
||
## 法律页面、版权与来源声明
|
||
|
||
- 前台提供公开静态法律页面:
|
||
- `/privacy-policy`:隐私政策。
|
||
- `/terms-of-service`:服务条款。
|
||
- `/disclaimers`:免责声明、第三方来源和权利归属说明。
|
||
- 法律页面只展示站点政策、来源和版权相关文案,不提供编辑表单、后端 API、数据库模型、管理入口或用户提交流程。
|
||
- 全局 `AppShell` 页脚展示:
|
||
- `Copyright {year} Tootaio Studio. All rights reserved.`
|
||
- Privacy Policy、Terms of Service、Disclaimers 链接。
|
||
- PokeAPI 数据与图片资源、社区贡献和 Pokemon 相关权利归属的简短说明。
|
||
- Pokopia Wiki 不是 Nintendo、The Pokemon Company、Game Freak、Creatures、PokeAPI 或 `pokopiawiki.com` 的官方、附属、赞助或背书项目。
|
||
- Pokopia Wiki 会使用或参考 PokeAPI 数据、PokeAPI 图片资源、`https://www.pokopiawiki.com/` 和其他公开资料;页面必须清楚说明引用来源不代表从属、赞助、背书或官方认可。
|
||
- Pokemon 相关名称、图片、标志、角色和游戏素材归其各自权利人所有。
|
||
- 法律页面和页脚文案必须通过系统级文案 catalog 管理,并支持现有语言回退机制。
|
||
|
||
## 项目更新展示
|
||
|
||
- Home 首页可展示 Pokopia Wiki 站点项目的公开更新信息,用于让访客了解站点代码与发布进展。
|
||
- 完整项目更新页路径为 `/project-updates`,由 Home 首页项目更新预览区的 View All 入口进入。
|
||
- 更新信息来源为公开 Gitea 仓库 `https://git.tootaio.com/Kingsmai/pokopiawiki.tootaio.com`。
|
||
- 前端不得直接读取 Gitea API;后端通过 `GET /api/project-updates` 代理并净化公开仓库数据。
|
||
- 项目更新 API 只返回展示所需字段:
|
||
- 仓库:`name`、`fullName`、公开仓库 `url`、`defaultBranch`、`updatedAt`。
|
||
- 最近提交分页:`items`、`nextCursor`、`hasMore`;每条提交只包含 `sha`、`shortSha`、提交标题 `title`、完整提交消息 `message`、`createdAt`、不含邮箱的 `authorName`、公开提交 `url`。
|
||
- 发布版本:`tagName`、`name`、`publishedAt`、公开发布 `url`。
|
||
- 最近提交支持 `limit` 和不透明 `cursor` 增量读取;前端不得依赖 Gitea 的 `page` / `limit` 实现细节。
|
||
- 项目更新 API 不返回 Gitea token、用户邮箱、内部 API URL、内网地址、文件列表、提交统计、Actions 日志、构建日志或调试字段。
|
||
- Home 首页默认展示最近提交预览;用户可通过 View All 进入 `/project-updates` 完整页面。
|
||
- `/project-updates` 按 Life Post 相同的增量方式继续显示更多提交。
|
||
- `/project-updates` 的每条提交默认折叠,仅展示标题、短 SHA、作者和时间;用户可展开单条提交查看完整 Commit Message,并可再次收起。
|
||
- 若仓库后续提供 Release,可展示发布版本。没有 Release 时不展示空发布区块。
|
||
- Gitea 读取失败时不得在前台展示内部错误或调试信息。
|
||
|
||
## 前端交互与 UI
|
||
|
||
- UI 风格以 `DesignGuidelines.html` 为准。
|
||
- 页面结构以 `AppShell`、`PageHeader`、列表、详情区和管理区为核心。
|
||
- 全局主导航使用 `AppShell` 侧边栏;移动端通过导航按钮打开侧边栏抽屉。
|
||
- 管理入口在全局侧边栏中保持单一 Admin 入口,`/admin` 内部使用页面内二级菜单分组组织管理模块:
|
||
- 配置:System config。
|
||
- 内容:Daily CheckList、Pokemon、物品、材料单、栖息地的维护、排序或删除入口,以及 Data Tools;Pokemon 在 Admin 中可删除但不提供手动排序。
|
||
- 内容管理包含 Items、Event Items 与 Ancient Artifacts;Items / Event Items 使用同一物品数据模型,通过 `is_event_item` 拆分入口。
|
||
- 本地化:Languages、System wordings。
|
||
- 访问权限:Users、Roles、Permissions、Rate limits。
|
||
- 登录用户的侧边栏账号入口进入 `/profile`;User Profile 属于账号入口,不作为 Wiki 主内容导航项。
|
||
- 页面级分类、筛选或辅助内容切换使用 Tabs,避免在内容页继续增加侧边栏。
|
||
- 导航和主要操作使用图标增强识别。
|
||
- 数据加载状态使用 Skeleton,避免裸文本 loading。
|
||
- 分类切换使用 Tabs。
|
||
- 布尔或模式选择使用 SwitchGroup、checkbox、segmented control 等合适控件。
|
||
- 多选和单选复用 `TagsSelect`,支持搜索、键盘操作和必要时的内联创建。
|
||
- 主要实体的新建和编辑使用路由驱动的 Modal:
|
||
- `/pokemon/new`
|
||
- `/event-pokemon/new`
|
||
- `/pokemon/:id/edit`
|
||
- `/habitats/new`
|
||
- `/event-habitats/new`
|
||
- `/habitats/:id/edit`
|
||
- `/items/new`
|
||
- `/event-items/new`
|
||
- `/items/:id/edit`
|
||
- `/ancient-artifacts/new`
|
||
- `/ancient-artifacts/:id/edit`
|
||
- `/recipes/new`
|
||
- `/recipes/:id/edit`
|
||
- `/ancient-artifacts/new` 和 `/ancient-artifacts/:id/edit` 使用 Items 编辑器与 Items create/update 权限;保存的是同一条 `items` 记录。
|
||
- Life 使用信息流顶部 New Post / 编辑按钮打开普通 Modal 发布与编辑,不使用路由驱动 Modal。
|
||
- 进入或关闭编辑 Modal 时应保留底层页面上下文,不进行不必要的滚动跳转。
|
||
- 用户界面不得展示内部字段名、调试数据、计划说明或“已修改某字段”一类实现说明。
|
||
- 权限不足时前端可以隐藏或禁用对应操作;后端必须返回本地化 403,并且不得在 UI 暴露内部权限 key 作为普通用户提示。
|
||
|
||
## Technical SEO
|
||
|
||
- 前端发布基础 SEO 静态资源:
|
||
- `favicon.ico`
|
||
- 默认社交分享图
|
||
- 品牌 Logo 素材
|
||
- `NUXT_PUBLIC_SITE_URL` 定义 canonical、Open Graph URL、robots sitemap 地址和 sitemap URL 的站点根地址;当前公开站点为 `https://pokopiawiki.tootaio.com`,本地前端端口默认使用 `http://localhost:20015`。
|
||
- 前端 Nuxt app head 配置提供默认 title、description、robots、canonical、Open Graph、Twitter card 和 favicon;路由 metadata 与详情页公开业务数据通过 Nuxt `useHead` 更新页面 metadata,避免直接操作 `document.head`。
|
||
- 主要公开浏览入口可索引:
|
||
- `/pokemon`
|
||
- `/event-pokemon`
|
||
- `/habitats`
|
||
- `/event-habitats`
|
||
- `/items`
|
||
- `/event-items`
|
||
- `/ancient-artifacts`
|
||
- `/recipes`
|
||
- `/checklist`
|
||
- `/life`
|
||
- `/life/:id`
|
||
- `/threads`
|
||
- `/threads/:threadId`
|
||
- `/project-updates`
|
||
- `sitemap.xml` 输出 sitemap index,并引用按公开模块拆分的全量 sitemap:
|
||
- `/sitemap-static.xml`:稳定公开顶层浏览入口和法律页面。
|
||
- `/sitemap-pokedex.xml`:Pokemon 详情页,URL 使用 canonical `/pokemon/:id`。
|
||
- `/sitemap-habitats.xml`:Habitat 详情页,URL 使用 canonical `/habitats/:id`。
|
||
- `/sitemap-collections.xml`:Items、Ancient Artifacts 和 Recipes 详情页,URL 分别使用 canonical `/items/:id`、`/ancient-artifacts/:id` 和 `/recipes/:id`;带 Ancient Artifact 分类的 item 只输出 `/ancient-artifacts/:id`,避免同一内容在 sitemap 中重复提交。
|
||
- `/sitemap-life.xml`:公开可见 Life Post 详情页,URL 使用 canonical `/life/:id`。
|
||
- `/sitemap-threads.xml`:公开 Thread 详情页,URL 使用 canonical `/threads/:threadId`。
|
||
- Sitemap URL 条目输出 `lastmod` 和 `priority`;详情页 `lastmod` 优先使用公开列表数据中的 `updatedAt` 或活跃时间字段,缺失时回退到 `createdAt`,不得暴露编辑人、权限、审核原因、内部审计 payload 或调试信息。
|
||
- 当前不输出公开 Profile 全量 sitemap;公开 Profile 可通过站内搜索和站内链接发现,避免将用户目录作为 sitemap 枚举面。
|
||
- Pokemon、物品、材料单和栖息地详情页在公开详情数据加载完成后,用实体名称、公开展示图片和本地化 SEO 文案更新 title、description、canonical、Open Graph 和 Twitter card。
|
||
- Threads 列表页使用 `/threads` canonical 并进入 sitemap;Thread 详情页在公开 Thread summary 加载完成后,用 Thread 标题、公开消息数、语言、标签、作者展示名和活跃时间更新 title、description、canonical、Open Graph 和 `DiscussionForumPosting` 结构化数据。
|
||
- 认证、管理、新建、编辑和开发中入口必须设置 `noindex`,避免搜索引擎索引受保护、低价值或临时流程页面。
|
||
- 新建页面 canonical 指向对应列表页;编辑 Modal 路由 canonical 指向对应实体详情页。
|
||
- SEO metadata 只能使用公开业务数据和系统文案;不得暴露邮箱、权限 key、token/hash、内部审计 payload、调试信息、未审核 Thread Message、审核原因或实现说明。
|
||
- 多语言 metadata 使用当前前端语言和系统文案回退机制;当前没有语言专属 URL,因此暂不输出 `hreflang`。
|
||
|
||
## 部署与升级维护
|
||
|
||
- Docker 部署时公开前端端口由 `frontend_gateway` 承载,正常流量代理到 `frontend` 服务。
|
||
- 前端浏览器 API base URL 由 `NUXT_PUBLIC_API_BASE_URL` 提供。
|
||
- Nuxt 服务端 API base URL 由 `NUXT_SERVER_API_BASE_URL` 提供;在 Docker 内默认使用 `http://backend:3001`,本地非 Docker 运行可使用 `http://localhost:3001`。服务端公开数据读取使用该内部地址,浏览器请求继续使用 `NUXT_PUBLIC_API_BASE_URL`。
|
||
- 前端 Docker 构建使用 Nuxt server output,`frontend` 服务通过 Node 运行 `.output/server/index.mjs`;Nuxt SSR server 监听容器内 `0.0.0.0:20015`,公开流量仍由 `frontend_gateway` 代理。
|
||
- `frontend` 因 `docker compose up -d --build` 重建、启动中或临时不可达时,`frontend_gateway` 返回静态升级维护页并保持公开端口可访问;后端 `/health` 不可用时,前端网关也返回同一维护页,避免用户看到静态页面后遇到 API 不可用。
|
||
- 升级维护页是基础设施级静态 fallback,不依赖 Vue、Vue I18n、后端 API 或数据库;页面只展示正式用户文案和品牌视觉,不展示构建日志、调试信息、内部字段或实现说明。
|
||
- 升级维护页使用 `503`、`Retry-After: 300`、`Cache-Control: no-store` 和 `noindex`,提示用户 Pokopia Wiki 正在升级并将在约 5 分钟内恢复。
|
||
- 本地 Docker 调试使用 `docker-compose.debug.yml`,通过 bind mount 运行 Nuxt dev server 与 backend `tsx watch`,支持前后端热重载;该调试入口不经过 `frontend_gateway` 维护页,不代表生产部署行为。
|
||
|
||
## API 概览
|
||
|
||
公开浏览 API:
|
||
|
||
- `GET /api/languages`
|
||
- `GET /api/system-wordings`
|
||
- `GET /api/options`
|
||
- `GET /api/project-updates`:读取站点项目公开更新信息;支持 `cursor` / `limit` 分页读取最近提交;仅返回净化后的仓库、最近提交和发布版本展示字段。
|
||
- `GET /api/daily-checklist`:公开页面支持 `cursor` / `limit` 分页读取;未传分页参数时返回完整数组以兼容管理端排序。
|
||
- `GET /api/pokemon`:支持 `isEventItem=true|false` 按普通 Pokemon / Event Pokemon 拆分列表;公开页面支持 `cursor` / `limit` 分页读取;未传分页参数时返回全部 Pokemon 以兼容管理端和实体选择器。
|
||
- `GET /api/pokemon/:id`
|
||
- `GET /api/habitats`:支持 `isEventItem=true|false` 按普通栖息地 / Event Habitats 拆分列表;公开页面支持 `cursor` / `limit` 分页读取;未传分页参数时返回全部栖息地以兼容管理端和实体选择器。
|
||
- `GET /api/habitats/:id`
|
||
- `GET /api/items`:支持 `isEventItem=true|false` 按普通 Items / Event Items 拆分列表;默认返回所有物品,包括已配置 Ancient Artifact 分类的物品;传入 `ancientArtifactCategoryId` 时可额外筛选对应 Ancient Artifact 分类下的物品;公开页面支持 `cursor` / `limit` 分页读取;未传分页参数时返回完整数组以兼容管理端、实体选择器和排序。
|
||
- `GET /api/items/:id`
|
||
- `GET /api/ancient-artifacts`:支持 `search`、`categoryId` 和 `tagIds` 筛选;公开页面支持 `cursor` / `limit` 分页读取;未传分页参数时返回完整数组以兼容排序。
|
||
- `GET /api/ancient-artifacts/:id`
|
||
- `GET /api/recipes`:公开页面支持 `cursor` / `limit` 分页读取;未传分页参数时返回完整数组以兼容排序。
|
||
- `GET /api/recipes/:id`
|
||
- `GET /api/dish`
|
||
- `GET /api/life-posts`:支持 `cursor` / `limit` 分页读取;支持 `search` 按 Life Post 正文搜索;支持 `language` 按审核语言区筛选,`all` 表示全部语言区;支持 `gameVersionId` 按 Game Version 筛选;支持 `sort` 为 `latest` 或 `oldest`。
|
||
- `GET /api/life-posts/following`:需要登录;分页读取当前用户已 Follow 用户发布的 Life Post 动态,支持与 Life Feed 相同的 `cursor` / `limit`、搜索、语言、Game Version 和排序筛选。
|
||
- `GET /api/life-posts/:id`:读取单条 Life Post 详情,遵守软删除和审核可见性规则。
|
||
- `GET /api/life-posts/:id/reactions`:分页读取该 Life Post 的公开 Reaction 用户列表;支持 `cursor` / `limit` 和 `reactionType` 筛选。
|
||
- `GET /api/life-posts/:postId/comments`:支持 `cursor` / `limit` 分页读取 Life Post 评论;支持 `language` 按审核语言区筛选;支持 `sort` 为 `oldest`、`latest`、`most-liked` 或 `most-replied`。
|
||
- `GET /api/thread-channels`:读取公开 Channel 列表,登录用户可同时得到 Follow Thread 的未读摘要。
|
||
- `GET /api/threads`:支持 `cursor` / `limit` 分页读取 Thread;支持 `channelId`、`language`、`tagId` 和 `sort`(`last-active`、`latest`、`most-discussed`)。
|
||
- `GET /api/threads/:id`:读取单个 Thread 详情。
|
||
- `GET /api/threads/:id/messages`:读取 Thread 消息;默认返回最新一页,支持 `before` / `limit` 向上加载历史。
|
||
- `POST /api/threads/ws-ticket`:创建短期一次性 Thread WebSocket ticket;需要登录。
|
||
- `GET /api/threads/ws?ticket=...`:Thread WebSocket 连接;只接收短期一次性 ticket。
|
||
- `GET /api/users/:id/profile`:读取公开用户 Profile 摘要、Wiki 贡献统计、公开社区统计和公开 Follow 统计;登录用户读取时返回自己与目标用户的关系状态。
|
||
- `GET /api/users/:id/life-posts`:分页读取该用户发布过且未删除的 Life Post。
|
||
- `GET /api/users/:id/reactions`:分页读取该用户设置过 Reaction 且目标未删除的 Life Post。
|
||
- `GET /api/users/:id/comments`:分页读取该用户未删除的 Life 评论和实体讨论评论。
|
||
- `PUT /api/users/:id/follow`:需要 `users.follow`;Follow 指定用户并返回更新后的公开 Profile。
|
||
- `DELETE /api/users/:id/follow`:需要 `users.follow`;Unfollow 指定用户并返回更新后的公开 Profile。
|
||
- `GET /api/discussions/:entityType/:entityId/comments`:支持 `cursor` / `limit` 分页读取实体讨论;支持 `language` 按审核语言区筛选;支持 `sort` 为 `oldest`、`latest`、`most-liked` 或 `most-replied`;`entityType` 支持 `pokemon`、`items`、`recipes`、`habitats`、`ancient-artifacts`。
|
||
|
||
认证 API:
|
||
|
||
- `POST /api/auth/register`
|
||
- `POST /api/auth/verify-email`
|
||
- `POST /api/auth/login`
|
||
- `POST /api/auth/request-password-reset`
|
||
- `POST /api/auth/reset-password`
|
||
- `GET /api/auth/me`
|
||
- `PATCH /api/auth/me`:更新当前用户显示名;需要登录;只接收并返回当前用户必要字段。
|
||
- `GET /api/auth/referral`:读取当前用户 Referral 摘要;需要登录;返回 `referral`,其中只包含 `code`、`url`、`verifiedReferralCount`。
|
||
- `POST /api/auth/logout`
|
||
- `GET /api/notifications`:读取当前用户通知分页列表和未读数量;需要登录。
|
||
- `POST /api/notifications/ws-ticket`:创建短期一次性通知 WebSocket ticket;需要登录。
|
||
- `POST /api/notifications/:id/read`:标记当前用户自己的单条通知为已读;需要登录。
|
||
- `POST /api/notifications/read-all`:标记当前用户全部通知为已读;需要登录。
|
||
- `GET /api/notifications/ws?ticket=...`:通知 WebSocket 连接;只接收短期一次性 ticket。
|
||
|
||
权限管理 API:
|
||
|
||
- `GET /api/admin/users`:需要 `admin.users.read`
|
||
- `PUT /api/admin/users/:id/roles`:需要 `admin.users.update`;分配或移除 `owner` 还需要调用者本身是 Owner 且拥有 `admin.users.assign-owner`;所有角色变更受 `roles.level` 层级限制
|
||
- `GET /api/admin/roles`:需要 `admin.roles.read`
|
||
- `POST /api/admin/roles`:需要 `admin.roles.create`
|
||
- `PUT /api/admin/roles/:id`:需要 `admin.roles.update`
|
||
- `DELETE /api/admin/roles/:id`:需要 `admin.roles.delete`
|
||
- `PUT /api/admin/roles/:id/permissions`:需要 `admin.roles.update`
|
||
- `GET /api/admin/permissions`:需要 `admin.permissions.read`
|
||
- `POST /api/admin/permissions`:需要 `admin.permissions.create`
|
||
- `PUT /api/admin/permissions/:id`:需要 `admin.permissions.update`
|
||
- `DELETE /api/admin/permissions/:id`:需要 `admin.permissions.delete`
|
||
- `GET /api/admin/data-tools/summary`:需要 `admin.data.export` 或 `admin.data.import`
|
||
- `POST /api/admin/data-tools/export`:需要 `admin.data.export`
|
||
- `POST /api/admin/data-tools/import`:需要 `admin.data.import`
|
||
- `POST /api/admin/data-tools/wipe`:需要 `admin.data.import`
|
||
|
||
受权限保护的编辑 API:
|
||
|
||
- Pokemon、栖息地、物品、材料单的创建、更新、删除分别需要对应实体的 `create`、`update`、`delete` 权限。
|
||
- `GET /api/pokemon/fetch-options`:按搜索词返回 Pokemon CSV data 搜索结果;支持 `all=true` 返回完整候选列表供前端本地筛选;需要 `pokemon.fetch`;只返回 `id`、`identifier`、`name`。
|
||
- `POST /api/pokemon/fetch`:按 data identifier 或 Pokemon ID 查询 CSV 资料并填充 Pokemon 编辑表单;需要 `pokemon.fetch`;不直接保存 Pokemon。
|
||
- `POST /api/pokemon/image-options`:按 data identifier 或 Pokemon ID 查询 pokesprite 可用图片候选;需要 `pokemon.fetch`;只返回 `id`、`identifier` 和图片候选列表。
|
||
- `POST /api/uploads/:entityType`:上传 Wiki 图片;需要对应实体上传权限;`entityType` 支持 `pokemon`、`items`、`habitats`;返回图片历史记录项和可展示 URL。
|
||
- Life Post 的创建,以及作者本人对 Life Post 的更新、删除,需要对应 `life.posts.*` 权限;管理他人内容需要对应 `*-any` 权限。
|
||
- `POST /api/life-posts`
|
||
- `PUT /api/life-posts/:id`
|
||
- `DELETE /api/life-posts/:id`
|
||
- `POST /api/life-posts/:id/moderation/retry`
|
||
- Life Comment 的创建,以及作者本人对 Life Comment 的删除,需要对应 `life.comments.*` 权限;管理他人内容需要对应 `*-any` 权限。
|
||
- `POST /api/life-posts/:postId/comments`
|
||
- `POST /api/life-posts/:postId/comments/:commentId/replies`
|
||
- `DELETE /api/life-comments/:id`
|
||
- `POST /api/life-comments/:id/restore`
|
||
- `POST /api/life-comments/:id/moderation/retry`
|
||
- Life Comment 的点赞和取消点赞需要 `life.comments.like` 权限。
|
||
- `PUT /api/life-comments/:id/like`
|
||
- `DELETE /api/life-comments/:id/like`
|
||
- 实体讨论评论的创建、回复,以及作者本人对评论的删除,需要对应 `discussions.comments.*` 权限;管理他人内容需要对应 `*-any` 权限。
|
||
- `POST /api/discussions/:entityType/:entityId/comments`
|
||
- `POST /api/discussions/:entityType/:entityId/comments/:commentId/replies`
|
||
- `DELETE /api/discussions/comments/:id`
|
||
- `POST /api/discussions/comments/:id/moderation/retry`
|
||
- 实体讨论评论的点赞和取消点赞需要 `discussions.comments.like` 权限。
|
||
- `PUT /api/discussions/comments/:id/like`
|
||
- `DELETE /api/discussions/comments/:id/like`
|
||
- Thread 创建需要 `threads.create`。
|
||
- `POST /api/threads`
|
||
- Thread 编辑需要作者本人或现有 Thread 管理权限。
|
||
- `PUT /api/threads/:id`
|
||
- Thread Message 创建需要 `threads.messages.create`。
|
||
- `POST /api/threads/:id/messages`
|
||
- Thread Follow 需要 `threads.follow`。
|
||
- `PUT /api/threads/:id/follow`
|
||
- `DELETE /api/threads/:id/follow`
|
||
- `POST /api/threads/:id/read`
|
||
- Thread 和 Message Reaction 需要 `threads.reactions.set`。
|
||
- `PUT /api/threads/:id/reaction`
|
||
- `DELETE /api/threads/:id/reaction`
|
||
- `PUT /api/thread-messages/:id/reaction`
|
||
- `DELETE /api/thread-messages/:id/reaction`
|
||
- Thread 管理需要 `admin.threads.*` 权限。
|
||
- `GET /api/admin/thread-channels`
|
||
- `POST /api/admin/thread-channels`
|
||
- `PUT /api/admin/thread-channels/:id`
|
||
- `DELETE /api/admin/thread-channels/:id`
|
||
- `PUT /api/admin/threads/:id/lock`
|
||
- `DELETE /api/admin/threads/:id`
|
||
- `DELETE /api/admin/thread-messages/:id`
|
||
- Life Reaction 的设置、替换和取消。
|
||
- `PUT /api/life-posts/:id/reaction`
|
||
- `DELETE /api/life-posts/:id/reaction`
|
||
- 每日 CheckList 的创建、更新、删除、排序需要对应 `checklist.*` 权限。
|
||
- 全局配置项的查看、创建、更新、删除、排序需要对应 `admin.config.*` 权限。
|
||
- 限流设置的查看和更新通过 Access 权限控制:
|
||
- `GET /api/admin/rate-limits`:需要 `admin.rate-limits.read`
|
||
- `PUT /api/admin/rate-limits`:需要 `admin.rate-limits.update`
|
||
- 语言的查看、创建、更新、删除、排序需要对应 `admin.languages.*` 权限。
|
||
- 系统级文案的查看和更新需要对应 `admin.wordings.*` 权限。
|
||
- `GET /api/admin/system-wordings`
|
||
- AI 审核配置的查看和更新需要对应 `admin.ai-moderation.*` 权限。
|
||
- `GET /api/admin/ai-moderation`
|
||
- `PUT /api/admin/ai-moderation`
|
||
- `PUT /api/admin/system-wordings/:key`
|
||
- 物品、材料单、栖息地的列表排序需要对应实体的 `order` 权限;Pokemon 按 Pokopia 展示 ID(`display_id`)排序,不提供列表排序 API 或 Admin 手动排序入口。
|
||
|
||
## 开发与验证
|
||
|
||
- 本项目在 WSL 中开发,运行验证主要通过 Docker。
|
||
- 常规轻量验证:
|
||
- `pnpm lint`
|
||
- `pnpm typecheck`
|
||
- 不在 WSL 中运行测试作为完成任务的前置条件。
|
||
- Docker 运行问题以用户提供的 `docker compose up --build` 输出为准进行后续修复。
|
||
- 本地热重载调试可运行 `pnpm docker:debug` 或 `docker compose -f docker-compose.debug.yml up --build`;生产 SSR runtime 验证仍使用 `pnpm docker:prod` 或 `docker compose up --build`。
|