我先说结论AnyPS5 不是一个多复杂的项目它的本质就是一个“给自己用的游戏库记账本”。玩 PS5 到一定阶段最大的困扰根本不是“没游戏玩”而是“不知道下一款该玩什么”“哪张盘买了没通关”“愿望单里的游戏到底降没降价”。这些问题靠记忆和备忘录都解决不了因为它们需要一个结构化的个人数据库。我花了大概三周业余时间晚上和周末搭了这套工具。这篇文章不讲套话只想把做设计时怎么想的、写代码时怎么拆的、后期踩过哪些坑完整复盘一遍。无论你是 PS5 玩家想照着自己弄一套还是纯粹想找个练手项目学 Web 开发下面都应该有能直接抄的部分。1. 项目定位与整体设计思路1.1 动机游戏库管理的三个真实痛点先说说我自己的使用场景。我平时既有数字版游戏也买实体盘还经常在打折季顺手领一堆免费游戏。时间一长问题集中在三块。第一库存完全无感。数字版藏在账号里实体盘塞在柜子里我根本说不清自己到底拥有多少款游戏。第二游玩状态混乱。有些游戏打了 80% 放着再拿起来已经忘了操作和剧情有些白金了隔半年会想不起这游戏到底值不值得推荐。第三愿望单形同虚设。商店里加了一堆关注但商店本身不会主动告诉我哪款降到心理价位了我也不可能天天去刷。这些痛点的共性在于它们都需要一个“以我为中心”的数据视图。商店平台给的是“以商品为中心”的信息适合浏览但不适合做个人管理。于是 AnyPS5 的核心定位就很清楚了——它不做商城、不做社交、不做云账号体系只做一件事把玩家拥有的游戏、游戏的状态、以及想买的游戏统一管理在一个本地数据库里。数据完全归自己想导出就导出想改就改。1.2 功能边界刻意做“小”动手之前我先列了一个“绝对不做”的清单。很多人做个人项目容易失控觉得什么都该加。我一开始也动过加社区评论、好友动态、推荐算法的念头后来全部砍掉。原因很简单一个单人维护的工具每加一个功能都要同步增加数据维护成本和崩溃排查成本。最终我保留了五个功能模块游戏入库、状态管理、愿望单、价格记录、统计面板另外配了导入导出。这条边界花了两个晚上才想清楚事实证明非常值得——因为砍掉的东西越多真正留下的功能才能做得越顺手。设计原则上我坚持“离线和私有优先”。整个项目不部署任何云端服务所有数据都落在本机 SQLite 文件里平时断网也能正常使用。只有“价格更新”和“封面抓取”这两个动作需要联网而且都是按需触发不是后台常驻。这样即使某天外部数据源改版不可用了游戏库本身依然完整可用。1.3 技术选型为什么是这套组合技术选型这块我没有纠结太久理由很直接。后端用 Python 的 FastAPI因为我的主语言就是 Python写起来快而且它处理 JSON 非常顺手天然适合前端页面直接调接口。数据库用 SQLite因为这是一个单用户的本地工具并发量几乎为零SQLite 一个文件搞定备份和迁移比部署一套独立数据库省心得多。前端用 Vue 3 Vite我不需要服务端渲染也不需要复杂的工程化Vue 的响应式语法最适合我一晚上写一个页面的节奏。整体不引入容器、队列、缓存中间件理由和上面一样个人工具的第一原则是维护成本最小化。如果一定要给个类比AnyPS5 就像自己做一顿家常菜不需要上酒店后厨的那套蒸烤箱和分子料理设备。工具越少翻车概率越低。这也是我后面开发过程中体会最深的一点很多项目不是死在大方向上而是死在过于复杂的初期架构上。2. 核心数据结构与关键功能拆解2.1 游戏、版本、地区数据模型怎么拆才不后悔这是整个项目里我思考最久的部分。游戏实体和“游戏标题”之间不是一对一关系比如某款大作既有标准版、豪华版、年度版又有港版、日版、欧版还有数字版和实体盘的区别。如果把这些全揉进一张表后面每次查询都是灾难。我最后的方案是把数据分成两层。第一层是 games 表表示“一个游戏标题”这个抽象概念记录游戏的全名、所属系列、类型、发行日期、封面路径、简介等基础信息。注意这里不记录“我买了几份”只记录“这是一款什么游戏”。第二层是 copies 表表示“我实际拥有的一个副本”每个副本关联一个 game_id同时记录数字版还是实体版、什么区域、什么版本名、购买价格、入手渠道以及实体盘存放位置。这样设计的一个直接好处是统计“库里有几款游戏”查 games 表记录数统计“手上有哪几份实体盘”查 copies 表。两者口径不同但都能通过一次简单查询得到。如果一个游戏我数字版和实体盘都买了games 表里也只有一条记录copies 表里则有两行。这种“标题—副本”的父子结构是整个数据模型稳定性的基石。下面是 SQLAlchemy 里核心模型的关键部分。# models.py 节选 from sqlalchemy import Column, Integer, String, Text, DateTime, func from database import Base class Game(Base): __tablename__ games id Column(Integer, primary_keyTrue) title Column(String(200), nullableFalse, indexTrue) series Column(String(100), default) genre Column(String(50), default) release_date Column(String(20), default) cover_path Column(String(255), default) # 本地封面文件路径 summary Column(Text, default) created_at Column(DateTime, server_defaultfunc.now()) status_lookup Column(String(10), defaultbacklog) # 状态索引 progress Column(Integer, default0) # 0-100 进度copies 表的关键字段是 edition_name、edition_typedigital/disc、region、purchase_price。这里有个细节我把“当前游玩状态”放在 games 上而不是 copies 上。虽然理论上同一款游戏买了两份可能一份通关一份没通但实际使用中绝大多数人关心的还是“这游戏我通没通”。对少数双版本都有不同状态的情况我做了妥协统一以最新进度为准。这个决定简化了状态机复杂度代价只是丢失了一个几乎不会用到的细分维度非常划算。2.2 游玩状态和进度状态机要尽量简单在状态设计上我一开始列了六个状态未入库、想玩、正在玩、已通关、已白金、已弃坑。后来发现“未入库”其实是对“不在库里”的重复描述不如让库里只存在“我拥有或者我玩过”的游戏于是砍到五个想玩、进行中、已通关、已白金、已弃坑。状态之间允许任意跳转不设计强制顺序。有些游戏一周目通关后觉得没意思就弃坑了也有游戏搁置半年后又捡起来继续硬性流转反而是束缚。我给每个游戏只保留一个 progress 字段0-100 的整数白金直接就填 100。有人会问为什么不做自动同步奖杯接口因为我试过对接授权流程凭证维护麻烦而且奖杯列表往往包含大量我没有的 DLC 奖杯做出来可能比手动填还费劲。对个人项目来说手填 30 秒的事不值得用一个外部接口来替换。这里有个细节值得单独提状态变化时记录 updated_at统计面板里就可以按月份画出“通关曲线”。这个功能其实是顺手加的因为有了 updated_at统计逻辑就只是按日期分组计数代码不到二十行但用起来特别有成就感。我的数据里能看见哪个月集中通关了三款游戏哪几个月完全在摸鱼挺有意思。2.3 愿望单和价格记录小心“提醒”功能失控愿望单是游戏库的伴生模块。数据结构比游戏库简单wishlist 表里存游戏标题、目标价格、当前价格、历史最低价、备注以及一个 reached 布尔字段表示是否已达到目标价。价格数据怎么来我提供两种途径。一是手动更新打折季的时候自己看一眼填一下二是自动抓取通过发起带超时控制的 HTTP 请求从公开商店页面按标题搜索拿返回的 JSON解析出当前价和历史最低价的关键字段。抓取模块我写得非常保守每次请求之间至少休眠 5 秒失败就跳过并记录日志同一个游戏的抓取结果缓存一周。原因有两个高频请求必然导致访问被限制得不偿失价格本身不可能一天变好几次批量更新就放在每周手动触发一次。关于“提醒”我刻意没有接入邮件或者桌面通知而是在页面上用醒目颜色标注“低于目标价”。初衷是避免功能膨胀邮件需要额外配置 SMTP会引入新的故障点用户打开 AnyPS5 首页看到提醒本身已经足够。对个人工具来说价值感来自数据可用而不是通知有多即时。3. 实操记录从空目录到跑起来的 AnyPS53.1 环境初始化与项目结构我建议把整个项目放在一个目录里方便备份。以下是最终的项目结构主干一目了然。anyps5/ ├── backend/ │ ├── app/ │ │ ├── main.py # FastAPI 入口 │ │ ├── database.py # SQLite 连接与建表 │ │ ├── models.py # SQLAlchemy 模型 │ │ └── routers/ │ │ ├── games.py # 游戏库相关接口 │ │ ├── wishlist.py # 愿望单接口 │ │ └── stats.py # 统计接口 │ └── scripts/ │ ├── fetch_price.py # 价格抓取脚本 │ ├── export_data.py # 导出 JSON │ └── backup_db.py # 备份数据库 ├── frontend/ │ ├── src/ │ │ ├── pages/ # 列表页、详情页、统计页 │ │ └── components/ # 游戏卡片、状态标签等 │ └── package.json └── run.sh # 一键启动脚本环境搭建没什么玄学我用 venv 隔离 Python 环境前端用 npm 管理依赖两个服务分别启动。开发阶段只在本机地址跑不绑定多余端口也不做域名配置。你可能会觉得这简单得不像一个完整项目——确实如此。对个人工具来说把环境复杂度和运维复杂度压到最低等于给自己省了大把周末时间。3.2 元数据录入自动匹配为辅、手动修正为主游戏库最耗时间的不是写接口而是录入数据。我一开始天真地以为能从第三方数据库一键导入全部游戏清单实际上各家数据源要么接口权限复杂要么字段覆盖不全与其花时间对接维护不如先把主体流程跑通。我的实际录入流程分三步。第一步从公开商店页面按标题搜索拿到该游戏的名称、简介、封面图 URL 等基础 JSON 信息存入 games 表。第二步下载封面并保存为本地文件cover_path 直接指向本地路径。第三步手动补充发行日期、系列名以及我自己关心的主观标签比如“合家欢”“手柄体验好”。这个流程里最需要注意的是错误处理。搜索接口偶尔返回空结果或者相似标题我的策略是先展示匹配候选列表由用户确认后写入。任何一步网络请求出错都不影响已保存的数据。因为系统里的外部数据只是辅助录入真正的主数据以用户确认为准。这个设计让 AnyPS5 对网络状态不敏感实际体验很稳。封面下载也有细节。我会先把 URL 统一处理成带扩展名的文件名比如 1234_cover.jpg避免 URL 含动态参数时存成没有扩展名的脏文件。如果下载失败就留下空白路径前端显示占位图绝不阻塞录入流程。花几分钟处理的小逻辑能避免后续几百条记录的脏数据问题。3.3 后端接口与前端页面后端接口尽量 REST 化核心就几张表映射成几组接口。游戏库相关的接口大致是GET /api/games 查询游戏列表支持关键词、状态过滤 POST /api/games 新增游戏 GET /api/games/{id} 查询游戏详情 PUT /api/games/{id} 更新游戏信息 DELETE /api/games/{id} 删除游戏 POST /api/games/import 导入 JSON 数据前端页面做了三块列表页展示游戏卡片和快捷状态切换详情页展示简介、封面、副本列表和编辑表单统计页按状态、按年份、按月展示图表。列表页有一个关键交互快捷键。我给自己留了数字键 1-5 快速切换状态的操作不用进详情页就能完成日常维护。这个细节强烈推荐给做个人工具的朋友——自己用的工具交互密度可以高得离谱没人会觉得界面太“拥挤”。前端技术栈很普通组件化写法和大多数 Vue 项目一致。下面放一个最简单的游戏卡片组件片段作为参考。!-- GameCard.vue简化 -- template div classgame-card :classstatus- game.status_lookup img :srcgame.cover_path || placeholder :altgame.title / h3{{ game.title }}/h3 span classgenre{{ game.genre }}/span button click$emit(switch-status, game.id) 进度 {{ game.progress }}% /button /div /template说实话前端不是这个项目的重点CSS 我也没有写得很精细够用、能看、不别扭就行。真正让项目“好用”的是后端数据结构的合理性和快速的维护流程而不是花哨的界面。3.4 数据备份与导入导出数据是个人工具最值钱的部分我在备份上多花了一点心思。备份策略很朴素每天用 cron 调用备份脚本把 data 目录里的 SQLite 文件复制一份并加上日期后缀保留最近 30 份。因为 SQLite 本身就是单文件备份就是一次文件复制几乎零成本。导入导出用 JSON 格式理由是想完整保留嵌套结构。导出的内容分为 games、copies、wishlist 三块导入时执行三步操作先按 title 判断是否已存在不存在就新增存在则更新非空字段最后删除或者忽略重复的副本记录。为了防止误操作导入前会自动生成一份当前数据库快照万一导入结果不理想可以一键回滚。这个功能一开始其实没打算做后来发现在两台设备之间同步数据时特别需要它。如果把个人项目比作一个软件产品导入导出就是它的数据自主权。有了 JSON 导出就算以后不想用 AnyPS5 了所有数据也还在自己手里不会被任何工具绑架。4. 踩坑实录与排查思路4.1 同名游戏多版本归并的教训第一个大坑是我早就料想到、但初期没处理好的问题同名游戏出现多个版本。比如某款游戏发售时我录了标准版后来年度版打折我又买了年度版搜索结果一多库里就出现两条看起来很像但又不一样的记录。最麻烦的是这两条记录本身应该归并成同一个“游戏标题”才能正确统计“我拥有这款游戏”并把标准版和年度版两个副本挂在下面。初期全靠手工处理游戏一多就撑不住。最终我写了一个归并工具先把标题做规范化处理去掉空格、统一大小写、忽略语言后缀再用编辑距离阈值做相似度排序把疑似重复的记录列出来供人工确认。记录多了以后我大约手动确认了十几组合并之后每次录入新游戏都会先搜索一次避免重复造记录。这里最大的体会是数据模型的拆分和清洗工具一定要提前写。如果一开始把“标题”和“副本”混在同一张表里归并的代价会比现在大十倍。4.2 区域、语言和封面不一致第二个坑是区域差异。同一款游戏港版、日版、欧版的封面图、简介和标题语言都不一样。我最初统一用英文标题录入结果搜出来的全是英文封面完全失去了本地化的亲切感。后来我在 games 表外又加了一个 regions 字段存 JSON 数组记录“我关注哪些区域”同时让副本表每行都带 region 和 language 两个字段。前端默认显示优先区域找不到再显示备用区域的封面。这个改动看起来小但关系到使用感受——看到自己熟悉的封面比滚动一页英文封面舒服太多。所以我特别建议做类似工具的人从一开始就考虑“区域”这个维度不要等数据录进去再回头补。4.3 SQLite 加字段时的迁移麻烦第三个坑来自数据库变更。我中途给 games 表加了一个 completion_date 字段SQLite 的 ALTER TABLE 支持非常有限不能直接修改列定义只能 ADD COLUMN而且新列如果带默认值对已存在行的约束比较别扭。我踩坑的具体表现是加了字段之后老数据的该字段全是 NULL统计页在分组时出现了空值行。解决方法是写了一个简单的迁移脚本。# migrate_add_completion_date.py import sqlite3 conn sqlite3.connect(data/anyps5.db) try: conn.execute(ALTER TABLE games ADD COLUMN completion_date TEXT DEFAULT ) except sqlite3.OperationalError: pass # 字段已存在忽略 conn.commit() conn.close()这个脚本的核心是幂等重复执行不会出错。从此以后凡是改表结构的动作我都写一个独立的数字开头迁移脚本并记录当前 schema_version。虽然对小项目有点“杀鸡用牛刀”但经历过一次手工改表导致的数据不一致之后我觉得这个成本完全值得。4.4 更新数据时的稳定性和频率控制最后一个坑是关于外部数据更新的。最早写价格抓取的时候没有加合理的延时和缓存连续请求了几十次很快就返回错误码。我当时的体感是前一分钟还好好的突然之间就完全不可用了。修复方案正如前文所说每个请求之间至少间隔 5 秒所有结果缓存一周失败后做指数退避重试最多 3 次重试时延递增。改完之后再跑几十次都很稳定。我特别想提醒看到这篇文章的朋友任何依赖外部公开数据的工具都必须把“对方随时可能拒绝服务”当作默认假设去设计。延时、缓存、退避、失败降级这四样少一样采集代码就是一颗定时炸弹。5. 常见问题速查表5.1 高频问题与对应处理我把实际操作中遇到最多的问题整理成一张速查表方便直接对照。症状可能原因处理办法搜索结果为空网络失败或接口返回格式变化查看运行日志确认响应 JSON 字段是否变更必要时手动补录搜索到相似但不同的游戏标题匹配太宽松在候选列表中人工确认再决定写入还是归并封面图下载失败图片地址失效或网络超时给封面留空前端显示占位图后续手动补充同款游戏统计出现两次标题记录和副本记录混了检查是否在错误表里重复创建用归并工具清理导入数据后出现重复游戏导入时标题规范化不一致先建唯一索引再以标题加版本名作为合并键重新导入本地库文件太大封面图片存得太多定期清理无效封面文件用访问时间判断是否保留这张表里的问题多数来源于“数据源不稳定”和“数据模型不一致”两个根因处理方式都围绕“手动确认 幂等操作 容错降级”展开。5.2 提升日常使用幸福感的小建议上面属于排除故障的层面最后分享两个让日常使用更舒服的小技巧。第一个是给 run.sh 加一个简单的菜单式启动逻辑输入 1 启动后端输入 2 启动前端输入 0 全部停止。不要小看这个脚本它决定了你每次想打开游戏库时是“一条命令”还是“翻半天笔记”。第二个是每月初花五分钟做一次数据体检看统计页里游戏数量、已完成数量、愿望单目标价是否还准确。这个小习惯能防止数据库悄悄长出脏数据也让整个工具始终保持可信。我个人在实际操作中的体会是做 AnyPS5 最值得分享的经验就是一句话个人工具的核心是数据自己说了算功能只做刚需。我在写这个项目之前列过所谓核心功能清单后来发现真正每天都在用的只有四个——添加游戏、改状态、看愿望单、看统计。其余那些想象中很美好的功能上线半个月基本没碰过。最后再分享一个小技巧做这种个人管理工具千万不要从“完美数据模型”开始。先手动录十款游戏把最简单的列表页跑通然后拿着这个粗糙版本用上一周你自然而然就会发现真正需要优化的是哪个环节。手写的数据模型再难看只要你在真实使用它就会慢慢迭代成最适合自己的样子。这正是 AnyPS5 最让我满足的地方——它不漂亮、不花哨但每一个字段都符合我自己的使用习惯。希望你能找到属于自己的那个“小而顺手”的工具。