我的动态页

文档编号 06

06 - 个人动态页开发笔记

创建日期:2026-04-28
用途:/my_activity 独立页面的维护参考,包含阅读动态、ACG 动态(Bangumi)和 Steam 动态三个模块的实现细节

页面基本信息

项目内容
页面模板文件usr/themes/classic-22/my-activity.php
共享函数文件usr/themes/classic-22/inc/activity-functions.php
异步 API 端点activity-api.php(项目根目录)
Typecho 模板名个人动态
页面 slugmy_activity
访问地址https://kaiwen.work/my_activity
缓存目录usr/cache/(已在 .gitignore,服务器自动创建)

模块一:阅读动态(微信读书 Agent Gateway)

API 基本信息

项目内容
GatewayPOST https://i.weread.qq.com/api/agent/gateway
认证Authorization: Bearer WEREAD_API_KEY
请求版本skill_version: 1.0.4(可由 WEREAD_SKILL_VERSION 覆盖)
客户端usr/themes/classic-22/inc/weread-client.php
聚合与渲染usr/themes/classic-22/inc/weread-activity.php
缓存文件usr/cache/weread_activity.json
永久兜底usr/cache/weread_activity_fallback.json
缓存有效期6 小时

请求流程

  1. 第一波并行请求:

    • /readdata/detail,mode=monthly
    • /shelf/sync
  2. 立即过滤 books[].secret=1 的私密书籍和 readUpdateTime<=0 的未读书籍。
  3. 公开电子书按 readUpdateTime 降序取最近 WEREAD_BOOK_CANDIDATE_LIMIT 本(默认 24)作为候选池。
  4. 第二波并行请求候选池里每本书的 /book/getprogress(curl_multi 并发,总耗时≈最慢单请求)。
  5. 过滤总阅读时长低于 WEREAD_MIN_BOOK_READ_SECONDS(默认 1800 秒 = 30 分钟)的书;进度请求失败、无法确认时长的书也一并过滤。
  6. 剩余书籍按 readUpdateTime 降序取前 3 本展示;不足 3 本时有几本显示几本,0 本显示"暂无公开阅读记录"。
  7. 聚合月度统计、封面、作者、最近阅读时间、进度和 deepLink。

月度统计字段单位:

字段说明
totalReadTime本月总阅读时长,单位为秒
readDays本月有效阅读天数
dayAverageReadTime按已过去自然日计算的日均时长,单位为秒

/book/getprogress 关键字段(注意字段名):

字段说明
book.readingTime总阅读时长(秒),用于时长过滤。recordReadingTime 恒为 0,勿用
book.progress阅读进度百分比(0-100)
book.updateTime最近阅读时间戳,作为 last_read_time

隐私与链接规则

  • 私密书籍和未读书籍必须在排序和进度请求前过滤,禁止进入短缓存、fallback 或 HTML。
  • 只展示 books[] 电子书,不混入 albums[] 有声书。
  • 总阅读时长低于阈值或进度请求失败的书不进入展示,不因进度失败回退显示 --(候选池有 24 本兜底)。
  • 封面直接使用接口返回的 HTTPS cover;失败时复用活动卡片封面占位。
  • 书籍卡片只使用接口返回的 HTTPS deepLink,且 host 必须是 weread.qq.com 或其子域。
  • 缺少/非法 deepLink 时渲染不可点击卡片,不自行拼接链接。
  • 回包出现 upgrade_info 或 errcode != 0 时,本次刷新失败并读取 fallback。

模块二:ACG 动态(Bangumi API)

API 基本信息

项目内容
Base URL由 config.inc.php 中的 BANGUMI_API_BASE_URLS 配置,默认 fallback 为 https://api.bgm.tv
认证Authorization: Bearer BANGUMI_API_TOKEN(BANGUMI_TOKEN 亦可,二者等价)
Token 存放config.inc.php → define('BANGUMI_API_TOKEN', '...') 和 define('BANGUMI_TOKEN', BANGUMI_API_TOKEN)
Web 跳转由 BANGUMI_WEB_BASE_URL 配置,默认 https://bgm.tv
图片域名优先使用 bgmimg.anibt.net,失败时由浏览器依次尝试配置镜像、lain.bangumi.one 和原始 URL
缓存文件usr/cache/bangumi_activity.json
缓存有效期6 小时

调用接口

# 1. 获取最近 10 条收藏
GET /v0/users/xmicrox/collections?limit=10&type=0

# 2. 获取每个收藏的详细信息(含 infobox)
GET /v0/subjects/{subject_id}

关键字段说明

字段来源说明
name_cn / namecollections → subject中文名优先,fallback 日文名
images.commoncollections → subject封面图 URL
ep_statuscollections我的话数进度
vol_statuscollections我的卷数进度
ratecollections我的评分(0 = 未评)
rating.scoresubjects detail(非 collections)网站评分,collections 接口此字段可能为空
infoboxsubjects detail作者/出版社等元数据,值可能为字符串或嵌套数组

subject_type 枚举

值类型
1书籍
2动画
3音乐
4游戏
6三次元

infobox 提取逻辑

// 作者/导演:按优先级匹配 key
$authorKeys    = ['作者', '原作', '导演', '监督', '原案'];
// 出版/发行:按优先级匹配 key
$publisherKeys = ['出版社', '发行', '动画制作', '制作公司', '发行方', '开发'];

// 注意:value 可能是字符串,也可能是 [{v: '...', k: '...'}] 形式的数组

镜像 / 反代配置(2026-06-01)

Bangumi HTTP 通信复用 usr/themes/classic-22/inc/bangumi-client.php:

  • bangumiRequest() 按 BANGUMI_API_BASE_URLS 顺序请求,某个 API base 网络失败、非 2xx 或 JSON 非法时自动尝试下一个
  • bangumiWebSubjectUrl() 用 BANGUMI_WEB_BASE_URL 生成条目页跳转
  • bangumiImageUrlCandidates() 生成图片候选,保留 path/query/fragment 并去重
  • ACG 卡片加载失败时由浏览器依次尝试候选,全部失败后显示灰色占位块
  • 第三方 API 反代会接收 Bangumi token;仅在信任反代提供方时配置到优先级靠前的位置

config.inc.php 示例(不要在维护文档写入真实 token):

define('BANGUMI_API_BASE_URLS', [
    'https://api.bangumi.one',
    'https://bgmapi.anibt.net',
    'https://api.bgm.tv',
]);
define('BANGUMI_WEB_BASE_URL', 'https://bangumi.one');
define('BANGUMI_IMAGE_HOST_MAP', [
    'lain.bgm.tv' => 'bgmimg.anibt.net',
    'fast.bgm.tv' => 'bgmimg.anibt.net',
]);

已知问题 / 注意事项

  • rating.score 必须从 /v0/subjects/{id} 获取,collections 接口的 subject 对象中该字段有时为空
  • ep_status 优先于 vol_status 展示进度(有 ep_status 则显示话数,否则显示卷数)
  • 每次刷新数据需删除缓存:rm {TYPECHO_ROOT}/usr/cache/bangumi_activity.json

模块三:Steam 动态

API 基本信息

项目内容
Base URLhttps://api.steampowered.com
认证Query param ?key=STEAM_API_KEY
Key 存放config.inc.php → define('STEAM_API_KEY', '...')
SteamIDconfig.inc.php → define('STEAM_ID', '76561198141733567')
缓存文件usr/cache/steam_activity.json(+ steam_activity_fallback.json 永久兜底)
缓存有效期6 小时
缓存写入判据仅当 fetchSteamPlayerSummary() 成功($player !== null)才写入短缓存和 fallback。渲染端硬依赖 player 字段,若以"任一子接口成功"为判据会用 player=null 的半成品污染缓存,导致页面空白持续整个 TTL 周期。详见 00-dev-errors.md E15。

fallback 与接口异常边界(2026-07-27)

  • Steam fallback 只处理 Steam 上游数据请求失败:player 拉取失败时读取 steam_activity_fallback.json
  • fallback 数据仍需经过 render_steam_content();PHP 渲染异常不能由数据缓存兜底
  • activity-api.php 捕获 Throwable,清理输出缓冲并返回 HTTP 500 JSON,避免错误页 HTML 污染接口响应
  • 前端先读取响应文本再解析 JSON:HTTP/非 JSON 响应显示“服务响应异常”,只有 fetch() 本身失败才显示“网络错误”
  • Steam 离线时间由独立的 formatActivityTimeAgo() 格式化,避免删除其他模块时误删共享函数

调用接口

# 1. 玩家基本资料(头像、昵称、在线状态)
GET /ISteamUser/GetPlayerSummaries/v2/?key={key}&steamids={steamid}

# 2. 近期游戏(最多6条,近2周)
GET /IPlayerService/GetRecentlyPlayedGames/v1/?key={key}&steamid={steamid}&count=6

# 3. 徽章(player_level、badge_count)
GET /IPlayerService/GetBadges/v1/?key={key}&steamid={steamid}

personastate 枚举

值文字颜色
0离线#888888
1在线#57cbde
2忙碌#f8c84e
3离开#f8c84e
4打盹#f8c84e
5想交易#57cbde
6想游戏#57cbde

游戏封面 URL 拼装

⚠️ 旧格式对 2024 年后上架的新游戏返回 404,当前使用 IStoreBrowseService/GetItems 获取真实 URL。

当前实现(2026-04-29 更新):

调用 fetchSteamGameCovers()(activity-functions.php),批量向 api.steampowered.com/IStoreBrowseService/GetItems/v1/ 请求,从 assets.asset_url_format + assets.header 构造完整封面 URL:

CDN  = https://shared.akamai.steamstatic.com/store_item_assets/
URL  = CDN + asset_url_format.replace("${FILENAME}", assets.header)

示例(新格式):
https://shared.akamai.steamstatic.com/store_item_assets/steam/apps/4126220/8583acfa.../header.jpg?t=1776328466

示例(老格式,无 hash 段):
https://shared.akamai.steamstatic.com/store_item_assets/steam/apps/730/header.jpg?t=1749053861

兜底机制(封面 URL 404 时):onerror 将 <img> 替换为带 Steam 品牌渐变(#1b2838 → #2a475e)的 <div class="steam-cover-missing">。

游戏卡片 UI

游戏卡片使用全幅封面图 + 底部半透明渐变遮罩设计:

  • 卡片比例固定 aspect-ratio: 460 / 215(Steam header 图标准比例)
  • 封面图绝对定位铺满卡片(object-fit: cover)
  • 游戏名、时长信息通过 position: absolute; bottom: 0 + 渐变背景(rgba(0,0,0,0.75) → transparent)浮于底部
  • 无封面时显示深蓝渐变占位(.steam-cover-missing)

隐私前提

用户 Steam 个人资料及游戏详情须设为公开,否则 GetRecentlyPlayedGames 返回空数组。
验证字段:communityvisibilitystate = 3


页面布局结构

[折叠标题] ▶ 阅读动态          上次更新 2026-07-28 10:00
  <div id="weread-content">      ← 骨架屏占位,JS 异步注入
    ┌─────────┬─────────┬─────────┐
    │ 本月阅读 │ 阅读天数 │ 日均阅读 │  ← 三列等权统计
    └─────────┴─────────┴─────────┘
    ┌─────────┬─────────┬─────────┐
    │ 最近书籍 │ 最近书籍 │ 最近书籍 │  ← 真实封面 + 阅读进度
    └─────────┴─────────┴─────────┘

[折叠标题] ▶ ACG 动态          上次更新 2026-04-29 18:28
  <div id="acg-content">          ← 骨架屏占位,JS 异步注入
    ┌──────────┬──────────┐
    │  卡片1   │  卡片2   │  ← .activity-grid(2列 Grid)
    ├──────────┼──────────┤
    │  ...     │  ...     │  ← 10 条收藏
    └──────────┴──────────┘

[折叠标题] ▶ Steam 动态         上次更新 2026-04-29 19:10
  <div id="steam-content">        ← 骨架屏占位,JS 异步注入
    ┌─────────────────────────────┐
    │  [头像] 昵称  在线状态  Lv.XX│  ← .steam-profile(用户状态卡)
    ├─────────────────────────────┤
    │  近 2 周游戏                  │
    │  ┌───────────┬───────────┐  │
    │  │ [封面全幅] │ [封面全幅] │  │  ← .steam-games-grid(2列)
    │  │ ▓▓▓ 遮罩  │ ▓▓▓ 遮罩  │  │  ← 渐变遮罩覆盖底部
    │  │ 游戏名    │ 游戏名    │  │
    │  │ 时长      │ 时长      │  │
    │  └───────────┴───────────┘  │
    └─────────────────────────────┘

响应式断点: 900px(ACG 卡片和 Steam 游戏网格 2列 → 单列)


异步加载架构

页面采用前端 JS 异步加载,消除 API 调用阻塞首屏渲染。

数据流:

浏览器请求 /my_activity
    ↓(极快,仅输出骨架屏 HTML)
my-activity.php(无 API 调用)
    ↓ JS fetch(并行)
    ├── GET /activity-api.php?module=weread  → {"html": "..."}
    ├── GET /activity-api.php?module=bangumi → {"html": "..."}
    ├── GET /activity-api.php?module=steam   → {"html": "..."}
    ↓ JS 注入 innerHTML
    ├── #weread-content  ← 月度统计 + 最近三本公开书籍
    ├── #acg-content     ← ACG 动态卡片
    ├── #steam-content   ← 用户状态卡 + 游戏网格

关键文件:

文件职责
my-activity.php渲染骨架屏页面骨架 + CSS + JS 加载逻辑
activity-api.phpJSON API 端点,接收 ?module=weread/bangumi/steam,返回 {"html":"..."}
usr/themes/classic-22/inc/activity-functions.php所有数据函数 + render_bangumi_content() + render_steam_content()
usr/themes/classic-22/inc/weread-client.php微信读书 Gateway 鉴权、批量请求和响应校验
usr/themes/classic-22/inc/weread-activity.php微信读书数据聚合、缓存和 render_weread_content()

activity-api.php 响应格式:

// 成功
{ "html": "<div class=\"activity-grid\">...</div>", "updated_at": "2026-04-29 18:28" }

// 失败(非法 module)
HTTP 400  { "error": "invalid module" }

// 失败(模块执行异常)
HTTP 500  { "error": "module unavailable", "message": "服务暂时不可用,请稍后重试。" }

updated_at 来源规则:

数据来源updated_at 取值
实时拉取成功当前时间
6 小时短缓存命中短缓存文件的 filemtime
Fallback 命中Fallback 文件的 filemtime
注意: 服务器 PHP 时区需设为 Asia/Shanghai(在 /etc/php/x.x/fpm/php.ini 中设置 date.timezone = Asia/Shanghai),否则 date() 返回 UTC 时间。

配置文件(config.inc.php)

// 以下 token 存放在 config.inc.php(已在 .gitignore,不提交 git)
define('BANGUMI_API_TOKEN',   '...');  // 用于 ACG 动态模块 API 调用
define('BANGUMI_TOKEN', BANGUMI_API_TOKEN);  // 等价别名,供 rating API 体系使用
define('STEAM_API_KEY',       '...');  // 用于 Steam 动态模块 API 调用
define('STEAM_ID',            '...');  // Steam 用户 64位 ID
define('WEREAD_API_KEY',      '...');  // 微信读书个人 API Key
define('WEREAD_SKILL_VERSION', '1.0.4'); // 可选,默认已内置
define('WEREAD_MIN_BOOK_READ_SECONDS', 1800); // 可选,阅读动态展示书籍的最小总阅读时长(秒),默认 1800(30 分钟)

常用维护操作

# 清除 Bangumi 缓存(强制下次访问刷新数据)
rm {TYPECHO_ROOT}/usr/cache/bangumi_activity.json

# 清除 Steam 缓存
rm {TYPECHO_ROOT}/usr/cache/steam_activity.json

# 清除微信读书短缓存(强制下次访问刷新)
rm {TYPECHO_ROOT}/usr/cache/weread_activity.json

# 同时清除微信读书永久兜底
rm {TYPECHO_ROOT}/usr/cache/weread_activity_fallback.json

# 清除全部缓存
rm {TYPECHO_ROOT}/usr/cache/*.json

# 查看缓存状态
ls -la {TYPECHO_ROOT}/usr/cache/

# 检查 PHP 错误日志(如果页面异常)
tail -50 /var/log/nginx/error.log

后续扩展计划

  • [x] 页面保留阅读 + ACG + Steam 三模块异步加载