================================================================
        Web Push（iOS PWA）消息推送系统
                部署与操作说明书
================================================================

【项目简介】
  一套完整的 Web Push 推送演示/教学系统，支持 iPhone「添加到
  主屏幕」后接收推送（iOS 16.4+），也支持 Android/桌面 Chrome。
  无需数据库，单个 PHP 文件实现全部后端逻辑（RFC8291 负载加密、
  RFC8292 VAPID 签名），自带管理端发送页面。

【文件清单】
  index.php              后端主程序（API 路由 / 加密 / VAPID /
                         动态生成 sw.js、manifest、图标）
  app.html               接收端页面（iPhone 订阅用，路径 /app）
  send.html              管理端页面（发送通知，路径 / ）
  sw.js                  Service Worker（静态文件，必须存在！）
  icon.png               通知/应用图标 192x192
  apple-touch-icon.png   iOS 主屏幕图标
  favicon.ico            网站图标
  data/store.json        运行时自动生成：VAPID 密钥 + 订阅列表
                         （含私钥，切勿外泄，禁止公网下载）

【运行要求】
  1. PHP >= 7.3，需启用 openssl、curl 扩展（宝塔默认已启用）
  2. 必须 HTTPS（Service Worker / Web Push 的硬性要求）
  3. iPhone 接收推送需 iOS 16.4+，且页面必须以「主屏幕 PWA」
     方式运行（Safari 普通标签页没有通知能力）
  4. 服务器需能访问外网推送服务（web.push.apple.com、
     fcm.googleapis.com 等）

================================================================
一、宝塔面板部署（推荐）
================================================================

  1. 创建站点
     宝塔面板 → 网站 → 添加站点：
       - 域名：填你的域名（如 push.example.com）
       - PHP版本：选 7.3 以上（推荐 8.x），不要选「纯静态」
       - 数据库：不创建

  2. 上传文件
     将本项目全部文件上传到站点根目录：
       /www/wwwroot/你的站点目录/
     保持文件结构不变，sw.js、icon.png 必须在根目录。

  3. 设置伪静态（关键！）
     宝塔面板 → 网站 → 设置 → 伪静态，填入：

       location / {
           try_files $uri /index.php;
       }
       # 禁止外部下载运行时数据（内含推送私钥和订阅列表）
       location ^~ /data/ {
           deny all;
       }

     说明：try_files 让 /app、/api/*、/manifest.webmanifest 等
     虚拟路径都交给 index.php 处理。

  4. 申请 SSL 证书并开启强制 HTTPS
     宝塔面板 → 网站 → 设置 → SSL → Let's Encrypt 申请
     → 开启「强制HTTPS」。

  5. 检查目录权限
     确保 PHP 进程（www 用户）对站点目录有写权限，
     data/store.json 需要自动创建：
       chown -R www:www /www/wwwroot/你的站点目录
       chmod 755 /www/wwwroot/你的站点目录

  6. 验证部署（浏览器依次访问）
       https://你的域名/            → 应显示「推送管理端」页面
       https://你的域名/app         → 应显示「Web Push 接收端」
       https://你的域名/sw.js       → 应返回 JS 代码（HTTP 200）
       https://your域名/manifest.webmanifest → 应返回 JSON

  ★ 重要提醒（宝塔特有的大坑）：
     宝塔默认 nginx 配置含 location ~ .*\.(js|css)?$ 规则，
     所有 .js 请求会被拦截为纯静态文件，不会走 PHP。
     因此 sw.js 必须以真实静态文件存在于网站根目录
     （本包已提供）。若修改了 index.php 里的 SW 逻辑，
     记得同步更新静态 sw.js，否则浏览器注册的还是旧文件。
     同理 icon.png / apple-touch-icon.png / favicon.ico
     也是静态文件（本包已提供）。

================================================================
二、纯 nginx 手动部署（不用宝塔）
================================================================

  假设：站点目录 /www/wwwroot/webpush，域名 push.example.com，
  PHP-FPM 监听 unix:/run/php/php8.2-fpm.sock（按实际修改）。

  1. 安装 nginx + PHP（>=7.3，带 openssl、curl 扩展）+ PHP-FPM

  2. 上传项目文件到 /www/wwwroot/webpush

  3. nginx 配置示例（/etc/nginx/conf.d/webpush.conf）：

     server {
         listen 80;
         server_name push.example.com;
         return 301 https://$host$request_uri;
     }

     server {
         listen 443 ssl http2;
         server_name push.example.com;

         ssl_certificate     /etc/nginx/certs/push.example.com.pem;
         ssl_certificate_key /etc/nginx/certs/push.example.com.key;

         root  /www/wwwroot/webpush;
         index index.php;

         # 虚拟路径交给 PHP 路由
         location / {
             try_files $uri /index.php;
         }

         # 保护运行时数据（内含私钥）
         location ^~ /data/ {
             deny all;
         }

         # PHP-FPM
         location ~ \.php$ {
             try_files $uri =404;
             fastcgi_pass unix:/run/php/php8.2-fpm.sock;
             fastcgi_index index.php;
             include fastcgi.conf;
         }

         # 其余静态文件（sw.js / 图标）由 nginx 直接返回，
         # 与 PHP 动态输出内容保持一致即可
     }

  4. 检测并重载 nginx：
       nginx -t && nginx -s reload

  5. 按上面「宝塔部署」第 6 步验证。

  （可选）极简部署——不装 nginx，用 PHP 内置服务器：
       php -S 0.0.0.0:8090 index.php
     此模式下 sw.js/manifest/图标全部由 index.php 动态输出，
     无需静态文件。但生产环境不建议，且仍需 HTTPS
     （可用 frp/caddy 等做 TLS 终结）。

================================================================
三、使用说明
================================================================

  ┌─────────────── 接收端（iPhone）───────────────┐
  │                                                │
  │ 1. 系统要求 iOS 16.4 及以上                     │
  │ 2. 用 Safari 打开 https://你的域名/app          │
  │ 3. 分享 → 添加到主屏幕                          │
  │ 4. 从主屏幕图标打开（不能用 Safari 标签页）      │
  │ 5. 点「开启推送通知」→ 系统弹窗 → 允许            │
  │ 6. 页面显示：通知权限=已授权、Service Worker=    │
  │    已注册、推送订阅=已订阅，即订阅成功            │
  │                                                │
  └────────────────────────────────────────────────┘

  ┌─────────────── 管理端（发送）─────────────────┐
  │                                                │
  │ 1. 任意设备浏览器打开 https://你的域名/          │
  │ 2. 填写标题、内容 → 点「发送」                   │
  │ 3. 所有已订阅设备会收到系统通知                  │
  │                                                │
  └────────────────────────────────────────────────┘

  【接收通知的正确姿势（iOS 特性）】
  - iPhone 上把 PWA 切到后台或锁屏，再从管理端发送；
    PWA 在前台时 iOS 会抑制推送横幅。
  - 若锁屏不弹但通知中心（顶部下拉）里有记录：
    设置 → 通知 → 找到该应用 → 开启「允许通知」，
    勾选锁定屏幕/通知中心/横幅；
    若通知为「静默送达」，长按该通知 → 选「突出显示」。
  - 配对了 Apple Watch 且戴在手腕上时，锁屏通知会转到
    手表显示，手机不弹属正常现象。
  - 开启了专注模式（勿扰等）时通知可能被静默。

  【管理端 API（可供程序调用）】
  - GET  /api/config                获取 VAPID 公钥和订阅数
  - POST /api/subscribe             上报订阅
       body: {"subscription":{endpoint,keys:{p256dh,auth}}}
  - POST /api/send                  发送通知
       body: {"title":"标题","body":"内容","url":"/app"}
       （title 和 body 不能同时为空；url 为通知点击后打开的页面）

  【数据与密钥】
  - VAPID 密钥对首次运行时自动生成，保存在 data/store.json。
  - 若删除 data/store.json，会生成新密钥，此时所有旧订阅全部
    失效（推送服务校验 VAPID 公钥一致性），设备需重新订阅。
  - 建议把 index.php 顶部的 VAPID_SUBJECT 改成你自己的邮箱。

  【订阅生命周期】
  - 设备每次点「重新订阅」都会产生新订阅；旧订阅失效后推送
    服务返回 404/410，后端会自动从 store.json 中清理。

================================================================
四、常见问题排查（FAQ）
================================================================

  Q1: 报「Script sw.js load failed」
      → 浏览器访问 https://域名/sw.js 看是否 200。
      → 常见原因：nginx 把 .js 拦截为静态文件但文件不存在，
        确认根目录有真实 sw.js 文件。

  Q2: 管理端报「Unexpected token '<' ... is not valid JSON」
      → PHP 报错信息混入了 JSON 输出。
      → 排查：临时在 index.php 开头加
        ini_set('display_errors', '0');
      → 查看具体报错：看 nginx/PHP 错误日志。
      → 宝塔常见诱因：open_basedir 限制触发 Warning
        （本版本已用 @is_file() 修复）。

  Q3: 发送返回 201 但手机收不到
      → 201 只代表苹果/谷歌推送服务器「接收」了消息，不代表
        设备能解密显示。
      → 先排除手机设置问题（见「接收通知的正确姿势」）。
      → 若加密实现不符合 RFC8291，设备会静默丢弃且无任何报错
        （本版本已通过 http_ece 参考实现交叉验证修复：
        key_info = "WebPush: info\0" || ua公钥 || 服务器公钥）。

  Q4: 想换域名 / 迁移服务器
      → 见下节。

================================================================
五、迁移到其他服务器
================================================================

  【整站打包】（本 zip 即为完整项目，data/ 已剔除）
  包内文件：index.php、app.html、send.html、sw.js、
  icon.png、apple-touch-icon.png、favicon.ico、本说明书。

  【迁移步骤】
  1. 新服务器按第一/二节完成环境部署（域名可换）。
  2. 解压 zip 到新站点根目录。
  3. 分两种情况：
     a) 同域名迁移（保留旧订阅）：
        从旧服务器拷贝 data/store.json 到新站点 data/ 目录，
        设备无需重新订阅。
        （store.json 含 VAPID 私钥，传输注意保密）
     b) 新域名部署：
        无需任何额外操作，首次访问自动生成新密钥，
        各设备需重新「开启推送通知」。
  4. 验证：按第一节第 6 步逐项检查。

================================================================
  本说明书基于实测环境整理：
  iOS 18.7 / 宝塔 nginx + PHP 8.2 (PHP-FPM) / HTTPS
================================================================
