Add private operations portal and weekly accounting reports
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
# PawPlay 私有运营台
|
||||
|
||||
周报、预算和收支台账的只读网站。无前端构建、无第三方运行依赖,Node.js 22 及以上可直接运行。数据目录与代码、镜像分离;上传视频、采集数据和生成周报由独立任务完成。
|
||||
|
||||
## 配置
|
||||
|
||||
必需环境变量:
|
||||
|
||||
| 名称 | 内容 |
|
||||
| --- | --- |
|
||||
| `USERNAME` | 管理员账号,无默认值 |
|
||||
| `PASSWORD_HASH` | 下述脚本生成的 scrypt 哈希,无默认密码 |
|
||||
| `SESSION_SECRET` | 随机生成、至少 32 字符的独立会话密钥 |
|
||||
| `PUBLIC_ORIGIN` | 对外 HTTPS 源,例如 `https://pawplay-ops.example.com`,不带路径或结尾斜杠 |
|
||||
|
||||
`DATA_DIR` 默认 `./data`,容器内默认 `/data`。`PORT` 默认 `8080`,监听 `0.0.0.0`。
|
||||
|
||||
密码通过标准输入传给 `node scripts/hash-password.mjs`,不要放进命令参数、Git 或日志。该脚本输出 `scrypt$16384$8$1$<base64url-salt>$<base64url-key>`。要求密码至少 16 字符;环境文件权限应为 `0600`,只由管理员访问。Compose 的 `env_file` 中将哈希值包在单引号内,避免 `$` 被插值。
|
||||
|
||||
使用自己的凭据配置环境后,运行 `npm start`。本地 HTTP 预览可以显式设置 `COOKIE_SECURE=false`,同时将 `PUBLIC_ORIGIN` 指向准确的 `http://localhost:<port>` 或 `http://127.0.0.1:<port>`。该开关拒绝非回环地址;生产环境必须保留默认安全 Cookie。
|
||||
|
||||
认证会话有效期 4 小时。退出登录会立即撤销当前 Cookie;服务重启会撤销所有会话。更换密码时更新 `PASSWORD_HASH` 并重新启动此服务。网站没有在线重设密码入口,也不记录密码、Cookie 或登录请求体。
|
||||
|
||||
## 数据格式
|
||||
|
||||
所有金额使用数值或 `null`。`null` 表示未知,不应以 0 代替。不同币种、现金支出、固定套餐分摊和预估费用分开保存;套餐内 Token 用量不能重复计作现金支出。账户总用量不能直接归因到 PawPlay。
|
||||
|
||||
`config.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "PawPlay 运营台",
|
||||
"timezone": "Asia/Shanghai",
|
||||
"currency": "USD",
|
||||
"budget": {
|
||||
"subscription_usd": 200,
|
||||
"billing_period": "pending",
|
||||
"project_allocation_fraction": null,
|
||||
"extra_api_weekly_limit_usd": null
|
||||
},
|
||||
"links": {"product": "", "repository": ""}
|
||||
}
|
||||
```
|
||||
|
||||
`billing_period` 支持 `monthly`、`weekly`、`pending`;项目分摊比例范围由生成任务校验,未确认时使用 `null`。
|
||||
|
||||
`ledger.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"entries": [
|
||||
{
|
||||
"id": "cost-001",
|
||||
"date": "2026-11-03",
|
||||
"label": "本周套餐分摊",
|
||||
"category": "subscription",
|
||||
"amount": null,
|
||||
"currency": "USD",
|
||||
"basis": "allocated",
|
||||
"source": "计费周期和项目占比待确认"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
`basis` 支持 `cash`、`allocated`、`estimate`、`settled`。支持额外元数据,例如 `project`、`status`、`usage`;网站不据此重新计算金额。
|
||||
|
||||
`reports/2026-W45.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "2026-W45",
|
||||
"title": "第 45 周运营报告",
|
||||
"period_start": "2026-10-30T20:00:00+08:00",
|
||||
"period_end": "2026-11-06T20:00:00+08:00",
|
||||
"published_at": "2026-11-06T20:00:00+08:00",
|
||||
"summary": "本周进展摘要。",
|
||||
"highlights": ["完成的工作"],
|
||||
"risks": ["需要关注的问题"],
|
||||
"next_actions": ["下一周计划"],
|
||||
"decisions": ["已确认的决策"],
|
||||
"metrics": {"views": null, "downloads": {"value": null, "source": "尚未接入采集"}},
|
||||
"costs": {"cash_usd": null, "allocated_usd": null, "estimated_usd": null},
|
||||
"revenue": {"settled_cny": null, "estimated_cny": null}
|
||||
}
|
||||
```
|
||||
|
||||
文件名必须与 `id` 一致。编号只接受字母、数字、下划线和连字符,长度不超过 80 字符。页面按 `period_end` 倒序展示;`metrics` 支持嵌套对象,页面以可读字段呈现。周报 JSON 可在登录后下载。报告生成任务应使用临时文件写完后原子替换,避免读到半份 JSON。
|
||||
|
||||
`period_start` 和 `period_end` 可使用 ISO 日期时间,页面统一以北京时间显示。当前未结束周期的快照添加 `"kind": "snapshot"`,页面会注明“截至当前的部分周期”。
|
||||
|
||||
数据中不要放密码、Token、浏览器会话或签名密钥。文件下载受同样的认证保护。缺失文件按待核实展示;损坏 JSON 返回读取失败,不会静默按零处理。
|
||||
|
||||
## Docker 与反向代理
|
||||
|
||||
```sh
|
||||
docker build -t pawplay-ops .
|
||||
```
|
||||
|
||||
镜像内使用非 root 的 `node` 用户,内部端口 `8080`。生产 Compose 应将私有环境文件通过 `env_file` 注入,将报告目录挂载为 `/data:ro`,并确保该目录对容器的 UID 1000 可读。环境文件不挂载到 `/data`,不复制到镜像。该服务不需要写入报告目录;可设置只读根文件系统、`cap_drop: [ALL]` 和 `no-new-privileges`。
|
||||
|
||||
Nginx Proxy Manager 终止 HTTPS,转发到所选 NAS 端口。`PUBLIC_ORIGIN` 必须与用户访问的域名一致。反向代理无需读取报告内容或额外添加认证旁路。`GET /healthz` 可在 NAS 内网使用 HTTP 检查,只返回 `{"ok":true}`;它不代表报告采集已完成。
|
||||
|
||||
报告、台账以及配置的所有 API 均在服务端校验会话。报告不会作为静态资源提供。登录静态页面可匿名访问,不能从中读取项目运营数据。登录按连接来源限速;反向代理下来源可能合并,因此单人运营站点优先使用当前的保守限制。
|
||||
|
||||
## 检查
|
||||
|
||||
```sh
|
||||
npm test
|
||||
```
|
||||
|
||||
覆盖:缺配置时拒绝启动、密码散列、匿名隔离、登录与注销撤销、会话篡改、Secure Cookie、来源限制、登录限速、路径穿越和符号链接防护、异常请求体及金额空值保留。
|
||||
Reference in New Issue
Block a user