自托管PS5游戏库管理仪表盘:价格追踪与折扣提醒实战
游戏库里的数字版越堆越多真正通关的却没几个这种买游戏比玩游戏积极的状态很多 PS5 玩家应该都不陌生。AnyPS5 就是从我自己这种现实焦虑里长出来的一个自托管项目我的游戏分散在好几个区服账号里会免领了一堆打折买了不少却说不清库里到底有多少游戏、哪款打到一半、最想等的那几个作品最近有没有好价。与其继续靠脑内 Excel我干脆花了几周业余时间做了一套属于自己的 PS5 游戏库管理与价格追踪系统并把整个开发过程、踩坑记录和运维经验完整整理出来。简单说AnyPS5 是一个面向 PS5 玩家的私人游戏库管理仪表盘核心解决三件事把散落各处的数字版游戏统一管理起来持续追踪心仪游戏的价格和折扣不错过史低在不知道玩什么的时候给你一个有理有据的随机推荐。这篇记录适合有收藏习惯、对价格敏感的玩家也适合喜欢折腾自建服务的开发者——哪怕你不会写代码前半部分的需求拆解和思路也值得一看。1. 项目动机游戏库是怎么失控的1.1 三个区服账号一个混乱的家庭先说说我自己的情况。我的游戏并不是集中在一个账号里的——早期因为各种购入渠道不同日服、港服、美服各有一个常用账号会免游戏顺着账号往下领打折时哪个区便宜就切到哪个区买。几年下来游戏到底有多少没人说得清。真正让我决定动手的是一次对话朋友问我最近有什么游戏推荐我打开主机商店翻了半天发现很多我应该有的游戏根本记不起来是什么时候买的也记不清有没有通关。更别说价格了——同一个游戏在不同区服差价很大我印象里某个港服打完折的价格可能还不如另一个区的日常价。这种状态可以总结成三个具体痛点库存分散数字版、会免、试玩版混在一起主机自带的库只能按账号看跨区合并不方便。价格信息滞后想等打折就得时不时自己开网页查几个区服来回切换累而且容易漏。决策困难游戏多到一定程度现在玩什么反而成了一个需要思考的问题选择太多等于没有选择。AnyPS5 的第一版目标就是把这三个痛点一次性打掉。1.2 为什么不用现成的游戏管理平台开始之前我当然是先找了现成方案。市面上有一些游戏收藏管理工具也有人在用在线表格或者笔记软件维护游戏清单但我试下来都有点别扭。第一是手动录入负担大。几百个游戏一个个输进去还没开始管理就已经累死了。第二是价格追踪覆盖面有限有些工具只支持特定区服或者价格数据不是实时抓的等看到信息时折扣都结束了。第三是数据要交给第三方给一个陌生平台授权自己的游戏账号信息我心理上过不去。除此之外还有一个很现实的原因我本身是搞开发的与其在别人平台的限制里绕来绕去不如把数据握在自己手里。自建一个工具想怎么加功能就怎么加折腾本身就是这个项目的一大乐趣。1.3 技术选型怎么搭起来的AnyPS5 的整体结构并不复杂核心是采集 - 存储 - 计算 - 展示四段式我把它拆成四个模块采集模块负责从商店页面的数据接口拉取商品信息、价格、折扣、封面等元数据。存储模块关系型数据库保存游戏主数据、价格历史、游玩记录和通知设置。计算模块处理价格归一化、史低判断、随机推荐、通知触发等业务逻辑。展示模块一个轻量级的 Web 仪表盘让数据以直观的方式呈现在浏览器里。技术栈上后端选了 FastAPI。理由有三个一是 Python 写数据采集和定时任务很顺手生态里到处都是现成的库二是 FastAPI 自带接口文档开发调试效率高三是我需要一个异步 IO 模型下表现稳定的服务它在这方面很省心。数据库一开始用的是 SQLite因为零配置、数据文件也好备份。但后来价格历史表涨到几十万行偶尔会出现数据库锁的问题我就把数据库迁到了 PostgreSQL用容器编排一起管。前端部分我没有上重型框架用的是后端渲染模板加一点原生 JavaScript图表用一个轻量级绘图库——这个项目里我优先考虑的是自己看得懂、改得动而不是追求热门技术栈。提示如果你只是想给自己用不要一开始就把架构搞复杂。先把数据链路跑通再按需加组件是最稳的做法。我见过太多人第一天就规划微服务结果项目死在第七天。2. 数据采集游戏库与价格从哪来2.1 从商店页面背后拿到商品数据AnyPS5 最核心的数据源是商店网页版背后那些仅供网页端调用的数据接口。它们不是对开发者公开的 API但行为上就是普通的 HTTP 请求与浏览器访问无异。我在实现时遵循一个原则采集频率远比正常浏览低同时对接口返回做严格校验避免给对方服务器造成压力。请求的基本套路是这样先模拟浏览器的请求头带上语言和区域参数然后按分页拉取商品列表。每个商品会返回一堆字段包括标题、唯一标识、封面图、简介、原价、现价、折扣信息、发售日期等等。大部分字段存下来就能用。用 Python 写核心采集逻辑大概是这个样子import httpx BASE https://store.example.com/v2/products # 数据接口地址按实际部署调整 async def fetch_products(region: str, offset: int, size: int 50): headers { User-Agent: Mozilla/5.0 (compatible; AnyPS5Bot/1.0), Accept-Language: region_to_lang(region), } params {region: region, offset: offset, size: size} async with httpx.AsyncClient(headersheaders, timeout10) as client: resp await client.get(BASE, paramsparams) resp.raise_for_status() data resp.json() for item in data.get(items, []): yield normalize_game(item, region)这段代码的要点有两个。第一是每次只拉五十条页面数量多就靠 offset 翻页任何一次请求失败都不中断整个采集流程而是记录日志后跳过等待下一轮补采。第二是 normalize 这个函数它负责把接口返回的原始 JSON 转换成内部统一的数据结构比如价格字段从字符串转成整数分、把封面图 URL 规范化、给缺失的字段填默认值。2.2 价格历史每一笔快照都有价值价格追踪不能只存当前价格否则三天前打过折你都不知道。我的做法是单独建一张价格快照表每次采集跑完把每个游戏的当前价格和折扣状态插入一条新记录。随着时间积累这张表就成了价格历史可以直接画趋势曲线也能准确判断现在是不是史低。判断史低的 SQL 很简单思路就是对同一个游戏取历史记录里的最低价格SELECT game_id, MIN(price) AS lowest_price FROM price_snapshots WHERE game_id %s AND price 0 AND captured_at NOW() - INTERVAL 365 days注意这里的价格统一用整数分存储避免浮点数比较带来的精度问题。还有一个细节很多商店页面的原价在不同时期可能不同所以判断折扣率时我以当前记录里的原价为分母以历史最低实际在售价为参考线。这样即使某次接口返回的原价变了也不会计算出明显离谱的折扣率。我还会定期对价格快照做归档压缩。超过一年的明细数据按周聚合一次只保留每周最低价和最高价这样既能画长期趋势又不会让数据库无限膨胀。这个习惯在项目跑了几个月后发现特别有必要——不加控制的话每年光是价格记录就会新增几十万行。2.3 多区服合并同一款游戏怎么知道它们是同一个这是整个项目里最麻烦的部分。同一个游戏在不同区服的接口返回里标题、ID、价格都不一样不能直接用 ID 对齐。我采用的方案是三层归一化第一层尝试用发行商 全球唯一编号对齐很多游戏在不同区服共享一个内部编号。第二层用标题清洗后 发行商 发售年份做相似度匹配处理不同语言标题的问题。第三层剩余匹配不上的维护一张手工映射表。前两层覆盖了大约九成的情况剩下的一成靠人工补。这在数据工程里很常见——自动化能解决八成的工作最后的两成反而值得用心积累。映射表我做成 JSON 放在项目里每次采集对比发现新的映射关系就追加进去运行一年后基本趋于稳定。这里有个小坑标题清洗不能一刀切。日服游戏的日文标题和英文标题差别很大不能靠简单的去掉空格做匹配。我当时是先按语言分词再分别做标准化最后再加一道发行商发售年的强校验才把误匹配率压到可接受的范围。3. 核心功能模块实现3.1 游戏库状态管理给每个游戏一个当前状态数据采集解决的是我有哪些游戏但游戏库管理还需要解决这些游戏玩到哪了。我给每个游戏定义了一套状态机只有四种状态想玩领了、买了但还没开始或刚开头。在玩最近一段时间还在推进。已通关主线通关或者达成主要目标。已弃坑玩不下去或者明确不会再碰。白金奖杯单独作为一个属性记录不参与状态机流转。每次状态变更系统会记录变更时间和备注方便之后复盘我今年到底通关了几个游戏。状态管理看似简单真正用起来之后我发现有几个细节很重要。第一在玩状态需要自动冷却。比如一个游戏连续三十天没有新的游玩记录就自动回到想玩避免一堆游戏堆在在玩里显得好像都很活跃。第二导入游戏时默认状态是想玩绝不猜用户心思把所有判断交给用户手动调整。第三统计口径要稳定我自己在界面上只看两个核心指标通关率和平均通关时长其他花哨的数据都不放首页。3.2 今晚玩什么一个带权重的随机推荐这个功能是我个人最喜欢的也是朋友来家里时打开频率最高的页面。原理很简单把所有想玩和在玩的游戏放进一个池子按权重随机抽一个。权重由几个因素决定距离上次游玩的天数越久权重越高。通关预估时长短小精悍的作品更容易被推荐。手动置顶的最近想玩列表权重再翻倍。已经弃坑的游戏不出现在池子里。带权随机在代码层面非常直白关键在于权重的比例调参。我一开始把距离上次游玩天数的权重设得太高结果连续一周都在推荐同一个吃灰最严重的游戏体验很糟糕。后来加了一条规则同一游戏四十八小时内不能重复推荐同时把天数权重做了软化处理也就是超过六十天后权重不再线性增长而是封顶。这样推荐结果既照顾了吃灰游戏又不至于让人觉得这系统有毛病。实现时我用了一个最简单的累计权重法逻辑直观且不容易出错import random def weighted_choice(candidates): total sum(item[weight] for item in candidates) r random.uniform(0, total) upto 0.0 for item in candidates: upto item[weight] if upto r: return item为了让推荐更有说服力每次推荐同时给出三个理由比如你已经 83 天没打开它了预计通关时间 8 小时适合今晚这个系列你之前通关过两部。这个细节让一个简单的随机抽取变得像一位懂你的游戏搭子在帮你做决定。3.3 折扣通知在史低出现的时候喊你一声价格数据都存下来了如果还要人每天盯着网页看那就白干了。AnyPS5 的折扣通知模块支持两种触发方式一种是用户手动把某个游戏加入关注列表系统在其现价低于期望价格时提醒另一种是自动规则当任意游戏的折扣率超过 50% 且现价达到一年内的最低点时推送一条史低出现消息。通知去重是一个容易被忽略的坑。如果不做限制同一个游戏只要满足条件每次采集完都可能触发一次提醒一天下来能把人烦死。我的处理方式是持久化冷却记录每个关注条目在触发通知后进入二十四小时冷却期冷却期内即使再次满足条件也不重复推送冷却期结束且折扣还在可以再次提醒。冷却记录必须存数据库而不是内存否则服务一重启用户就会收到一波重复轰炸。这个问题我实际遇到过当时是半夜给某个配置了大额折扣的游戏做全量采集服务重启之后冷却状态全清空同一个游戏的史低提醒连着发了四条第二天起来看到消息列表整个人都清醒了。推送渠道我抽象成一个通知接口背后可以接任何支持 Webhook 的消息服务。好处是未来想换渠道只改一处配置就行不需要动业务逻辑。3.4 前端仪表盘把数据变成一眼能看懂的东西前端的目标很明确每一次打开 AnyPS5都能在十秒内回答三个问题——库里有什么、最近有什么好价、今晚玩什么。页面分四个区域。顶部是总览卡片显示游戏总数、通关数、正在游玩数、今年已花费金额。中间是价格热区展示最近两天出现好价或者达到史低的游戏列表按折扣率排序。左下是完整的游戏库表格支持按状态、区域、类型筛选也可以搜索。右下是随机推荐卡片点击按钮出结果附推荐理由。图表部分用轻量级绘图库画折线图展示每个关注游戏近九十天的价格走势。原本还想加一个购买时机打分后来想想太玄学就只保留原始价格曲线让数据自己说话。表格用了虚拟滚动几千行的库在浏览器里滚动也不卡。提示仪表盘最忌什么都往上堆。我第一版放了十几个图表实际每天都看的就那三四个后来干脆删掉一大半界面清爽多了。做个人工具克制比丰富更重要。4. 部署、运维与踩坑记录4.1 容器编排与定时任务部署我用容器编排管三个服务接口服务、价格采集任务、数据库。接口服务负责前端页面和业务 API采集任务通过定时调度触发。调度周期是每天凌晨跑一次增量采集只更新在售且近期有变化的游戏每周日晚上跑一次全量采集同步所有游戏的信息。全量采集大概四十分钟跑完增量采集几分钟内结束。定时任务我直接用系统 cron配合脚本触发容器内采集命令效果好理解0 3 * * * /usr/local/bin/anyps5 collect --incremental 0 4 * * 0 /usr/local/bin/anyps5 collect --full数据库备份是每周末做一次全量导出归档到另一个目录再同步到家里的存储设备。这里有个教训最初我以为 SQLite 备份只要复制文件就行后来发现如果复制时正赶上写入备份文件可能是坏的。换成 PostgreSQL 之后我用它自带的逻辑导出功能才彻底放心。4.2 限流与异常降级采集任务最怕两件事被封和被打。被封是指请求频率太高导致网络出口被限制访问被打是指接口返回异常数据导致库里出现脏数据。前者的应对手段是限速和重试退避比如正常请求间隔不少于两秒遇到限流状态码就停止当前批次等十分钟再继续。后者的应对手段是校验任何低于平台最低售价的价格、任何缺失标题的记录直接丢弃并写入采集日志绝不让脏数据进库。异常降级也很重要。有一次商店接口的目录结构调整采集任务连续失败两天界面上所有价格都还是旧数据。我一开始以为是 bug后来发现是接口字段变了。这让我养成了一个习惯采集模块的日志必须包含原始响应摘要一旦发现异常能快速判断是网络问题、接口变更还是解析逻辑出错。4.3 常见问题速查表把这段时间遇到的高频问题整理成一张表基本涵盖了自建这类工具会遇到的大部分情况。现象可能原因解决思路采集请求返回 403请求头缺少浏览器标识或区域参数错误补全请求头核对语言与区域映射同一游戏出现多条记录归一化匹配失败查看匹配日志补充映射表价格显示成 0.09 这样的小数货币单位判断错误统一按分存储按区域设定货币精度通知重复轰炸冷却记录只在内存冷却状态持久化到数据库定时任务时间不对时区没统一全部用 UTC 存储展示时再转本地数据库锁等待超时SQLite 并发写迁移 PostgreSQL或开启 WAL 模式封面图加载慢直接引用了外部图片地址本地镜像一份定期清理缓存这张表不是一次总结出来的大部分问题都是运行过程中踩进去又爬出来的。建议刚开始做类似项目的人第一天就把日志、备份、限速这三件事做好后面会省非常多事。5. 个人体会与后续方向AnyPS5 跑到现在已经是我生活中很稳定的一个基础设施。说句实话它带来的最大改变不是我再也没错过史低而是我重新恢复了对游戏库的掌控感。以前打开主机面对几百个游戏会愣住现在打开 AnyPS5 十秒钟内就知道该玩什么、哪些在等好价、今年通关了多少。这种自己的数据自己管的感觉比用任何第三方平台都踏实。有几个小经验想分享。一个是数据采集的时间策略价格折扣大多在商店每周例行更新时变化所以增量采集放在更新后一小时跑效果最好太早跑拿不到新数据太晚跑又会推迟通知。另一个是改数据结构时要留后路我栽过一两次——把价格字段从字符串改成整数存储时忘了旧数据里还混着小数格式结果统计图出现了一根吓人的尖峰。后续我想做三件事一是把采集扩展到更多区域商店形成更完整的全球比价视图二是基于游玩记录做一个简单的年度报告生成器年底自动输出今年你玩了 47 款游戏通关 12 款累计时长 380 小时这样的信息三是尝试在推荐引擎里引入分类偏好让随机推荐更贴合我的游玩习惯。如果你也有类似游戏库失控的困扰又恰好喜欢折腾我强烈建议你从最小版本开始先能抓到价格、存进数据库再慢慢往上加功能。项目最终会变成什么样往往是做着做着才清楚的。AnyPS5 对我来说就是这么一个越用越顺手的私人工具也希望这篇记录能给你一点自建这类系统的灵感。