# 帳票設計書 12-ニュースレター購読者統計

## 概要

本ドキュメントは、Ghost CMSにおけるニュースレター購読者統計レポートの設計仕様を定義する。このレポートは、特定のニュースレターに対する購読者数の現在値と日次増減の推移を、JSON形式でAPIエンドポイント経由で提供する。

### 本帳票の処理概要

本帳票は、ニュースレターごとの購読者数の変動を時系列で追跡し、読者基盤の成長や減少傾向を可視化するための統計データを提供する。

**業務上の目的・背景**：ニュースレターの購読者数は、パブリッシャーにとって重要なKPIである。購読者数の増減トレンドを把握することで、コンテンツ戦略の効果測定、購読離脱の原因分析、成長施策の効果検証が可能になる。本レポートは、日次ベースでの購読者数推移を提供し、これらの分析を支援する。

**帳票の利用シーン**：管理画面のStats（統計）セクションでニュースレターの購読者数推移を確認する際に利用される。特定のニュースレターを選択し、指定期間内の購読者数の日次推移グラフを表示することで、成長トレンドを把握できる。

**主要な出力内容**：
1. 現在の総購読者数（total）
2. 日次の累積購読者数推移（values配列）
3. 各日付（date）とその時点の購読者数（value）

**帳票の出力タイミング**：管理画面でニュースレター購読者統計APIエンドポイント（`GET /ghost/api/admin/stats/subscriber-count`）にアクセスした際にリアルタイムで生成される。

**帳票の利用者**：サイト管理者、コンテンツマーケター、グロースハッカー

## 帳票種別

統計レポート / 時系列データ（JSON形式）

## 利用画面

| 画面No | 画面名 | URL/ルーティング | 出力操作 |
|--------|--------|-----------------|---------|
| - | Stats画面 | `/ghost/#/stats/` | ニュースレター選択時に自動取得 |
| - | Subscriber Count API | `GET /ghost/api/admin/stats/subscriber-count` | APIリクエスト |

## 出力形式

### 基本仕様

| 項目 | 内容 |
|-----|------|
| ファイル形式 | JSON |
| 用紙サイズ | N/A（API出力） |
| 向き | N/A |
| ファイル名 | N/A（HTTPレスポンス） |
| 出力方法 | API経由でJSONレスポンス |
| 文字コード | UTF-8 |

### レスポンス構造

```json
{
  "data": [
    {
      "total": "number",
      "values": [
        {
          "date": "YYYY-MM-DD",
          "value": "number"
        }
      ]
    }
  ]
}
```

## 帳票レイアウト

### レイアウト概要

JSON配列形式で購読者統計を返却する。data配列には1つのオブジェクトが含まれ、現在の総購読者数と日次推移データを保持する。

```
{
  "data": [
    ┌─────────────────────────────────────┐
    │  total: 現在の総購読者数             │
    │  values: [                          │
    │    { date: "2024-01-01", value: 100}│
    │    { date: "2024-01-02", value: 105}│
    │    ...                              │
    │  ]                                  │
    └─────────────────────────────────────┘
  ]
}
```

### 明細部

| No | 項目名 | 説明 | データ取得元 | 表示形式 | 備考 |
|----|-------|------|-------------|---------|------|
| 1 | total | 現在の総購読者数 | members_newsletters（COUNT DISTINCT member_id） | 整数 | email_disabled=falseの会員のみ |
| 2 | values | 日次購読者数推移配列 | members_subscribe_events | 配列 | |
| 3 | values[].date | 日付 | members_subscribe_events.created_at | YYYY-MM-DD形式 | |
| 4 | values[].value | その日時点の累積購読者数 | 計算値 | 整数 | 日次増減から累積計算 |

## 出力条件

### 抽出条件

| 条件名 | 説明 | 必須 |
|-------|------|-----|
| newsletter_id | 特定のニュースレターIDでフィルタ | Yes |
| date_from | 開始日（YYYY-MM-DD形式） | No |
| date_to | 終了日（YYYY-MM-DD形式） | No |
| timezone | タイムゾーン（デフォルト: UTC） | No |

### ソート順

| 優先度 | 項目 | 昇順/降順 |
|-------|------|---------|
| 1 | date | 昇順 |

### 改ページ条件

N/A（JSON形式のため改ページなし）

## データベース参照仕様

### 参照テーブル一覧

| テーブル名 | 用途 | 結合条件 |
|-----------|------|---------|
| members_newsletters | 現在の購読者数取得 | newsletter_id = 指定値 |
| members | 購読者の状態確認 | members.id = members_newsletters.member_id |
| members_subscribe_events | 日次増減履歴 | newsletter_id = 指定値 |

### テーブル別参照項目詳細

#### members_newsletters

| 参照項目（カラム名） | 帳票項目との対応 | 取得条件 | 備考 |
|-------------------|----------------|---------|------|
| member_id | COUNT DISTINCT → total | newsletter_id = 指定値 | |
| newsletter_id | フィルタ条件 | | |

#### members

| 参照項目（カラム名） | 帳票項目との対応 | 取得条件 | 備考 |
|-------------------|----------------|---------|------|
| id | 結合キー | | |
| email_disabled | フィルタ条件 | email_disabled = false | 無効化されたメールを除外 |

#### members_subscribe_events

| 参照項目（カラム名） | 帳票項目との対応 | 取得条件 | 備考 |
|-------------------|----------------|---------|------|
| created_at | DATE() → values[].date | newsletter_id = 指定値 | |
| subscribed | 日次増減計算 | | 1=購読、0=解除 |
| member_id | email_disabled除外用 | | |
| newsletter_id | フィルタ条件 | | |

## 計算仕様

### 計算項目一覧

| 項目名 | 計算式 | 端数処理 | 備考 |
|-------|-------|---------|------|
| 日次増減 | SUM(CASE WHEN subscribed = 1 THEN 1 ELSE -1 END) | なし | 購読=+1、解除=-1 |
| 累積値 | 現在total - 期間内増減合計 + 累積 | なし | 逆算で開始値を求め、順次加算 |

### 累積値計算ロジック

1. 現在の総購読者数（total）を取得
2. 指定期間内の日次増減を集計
3. 期間内の総増減を計算
4. 開始時点の購読者数 = total - 総増減
5. 開始時点から日次増減を加算して累積値を算出

## 処理フロー

### 出力フロー

```mermaid
flowchart TD
    A[APIリクエスト受信] --> B{newsletter_id チェック}
    B -->|未指定| C[空結果返却]
    B -->|指定あり| D[パラメータ検証]
    D --> E[日付境界の計算]
    E --> F[総購読者数クエリ実行]
    F --> G[日次増減クエリ実行]
    G --> H[累積値計算]
    H --> I[欠落日付の補完]
    I --> J[結果をJSON形式で返却]
    J --> K[終了]
```

## エラー処理

### エラーケース一覧

| エラー種別 | 発生条件 | 表示メッセージ | 対処方法 |
|----------|---------|--------------|---------|
| データなし | newsletter_id未指定 | N/A | {total: 0, values: []} を返却 |
| 内部エラー | クエリ実行失敗 | ログ出力のみ | {total: 0, values: []} を返却 |

## パフォーマンス要件

| 項目 | 内容 |
|-----|------|
| 想定データ件数 | 日付数分（通常30-365日程度） |
| 目標出力時間 | 1秒以内 |
| 同時出力数上限 | 制限なし（通常のAPI制限に従う） |

## セキュリティ考慮事項

- Admin API認証が必須（mw.authAdminApi）
- posts権限のbrowseパーミッションが必要
- APIキャッシュによる負荷軽減（statsService.cache）
- 個人を特定できる情報（メールアドレス等）は含まれない
- email_disabled=trueの会員は集計から除外される

## 備考

- 日次増減の計算ではemail_disabled=trueの会員は除外される（NOT EXISTS サブクエリ使用）
- 指定期間内に購読イベントがない日は、前日の値を引き継いで補完される（_fillMissingDatesメソッド）
- 累積値は逆算で計算されるため、開始日より前のイベントも現在のtotalに反映されている
- moment-timezoneを使用してタイムゾーン処理を行う

---

## コードリーディングガイド

本帳票を理解するために参照すべきファイルと、推奨する読み解き順序を以下に示す。

### 推奨読解順序

#### Step 1: データ構造を理解する

まず、購読者統計で扱うデータ構造を理解することが重要。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | schema.js | `ghost/core/core/server/data/schema/schema.js` | members_newsletters, members_subscribe_eventsテーブルの構造 |

**読解のコツ**: `members_newsletters`テーブルはmember_idとnewsletter_idの多対多関係を管理し、`members_subscribe_events`は購読・解除イベントの履歴を保持する。

#### Step 2: エントリーポイントを理解する

処理の起点となるAPIルーティングとコントローラーを特定。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | routes.js | `ghost/core/core/server/web/api/endpoints/admin/routes.js` | subscriber-countのルート定義（行169） |
| 2-2 | stats.js | `ghost/core/core/server/api/endpoints/stats.js` | subscriberCountコントローラー（行290-317） |

**主要処理フロー**:
1. **行169**: `GET /stats/subscriber-count`ルート定義
2. **行290-317**: `subscriberCount`コントローラー、オプション（newsletter_id, date_from, date_to）を受け取りサービスを呼び出し

#### Step 3: サービス層を理解する

ビジネスロジックの中核となるサービスクラスを読み解く。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | stats-service.js | `ghost/core/core/server/services/stats/stats-service.js` | getNewsletterSubscriberStatsメソッド（行143-154） |
| 3-2 | posts-stats-service.js | `ghost/core/core/server/services/stats/posts-stats-service.js` | getNewsletterSubscriberStats実装（行937-1020） |

**主要処理フロー**:
- **stats-service.js 行143-154**: newsletter_idの抽出とサービス呼び出し
- **posts-stats-service.js 行943-957**: 総購読者数と日次増減の並列クエリ実行
- **posts-stats-service.js 行959-983**: 日次増減から累積値への変換
- **posts-stats-service.js 行997-1003**: 欠落日付の補完

#### Step 4: 日次増減クエリを理解する

プライベートメソッドの詳細を確認。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | posts-stats-service.js | `ghost/core/core/server/services/stats/posts-stats-service.js` | _getNewsletterSubscriberDeltasメソッド（行1031-1054） |

**主要処理フロー**:
- **行1035-1039**: SUM(CASE WHEN subscribed = 1 THEN 1 ELSE -1 END)で日次増減を計算
- **行1041-1046**: email_disabled=trueの会員をNOT EXISTS で除外

#### Step 5: 日付補完ロジックを理解する

欠落日付の補完処理を確認。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 5-1 | posts-stats-service.js | `ghost/core/core/server/services/stats/posts-stats-service.js` | _fillMissingDatesメソッド（行1066-1109） |

**主要処理フロー**:
- **行1075-1076**: moment-timezoneで日付範囲を生成
- **行1092-1106**: 各日付について、イベントがあれば累積値を更新、なければ前日の値を使用

### プログラム呼び出し階層図

```
routes.js (ルート定義)
    │
    └─ stats.js (APIコントローラー)
           │
           └─ subscriberCount
                  └─ statsService.api.getNewsletterSubscriberStats()
                          └─ StatsService.getNewsletterSubscriberStats()
                                  └─ PostsStatsService.getNewsletterSubscriberStats()
                                          │
                                          ├─ 総購読者数クエリ
                                          │      └─ members_newsletters + members
                                          │
                                          ├─ _getNewsletterSubscriberDeltas()
                                          │      └─ members_subscribe_events
                                          │
                                          └─ _fillMissingDates()
                                                 └─ moment-timezone
```

### データフロー図

```
[入力]                    [処理]                           [出力]

HTTPリクエスト ───▶ API Controller ───▶ StatsService ───▶ JSON Response
  (newsletter_id,        (パラメータ抽出)     (クエリ構築・実行)     { total, values }
   date_from,
   date_to)
                              │
                              ▼
                    ┌──────────────────┐
                    │   Database       │
                    │ ├─ members_      │ ──▶ 現在の総購読者数
                    │ │   newsletters  │
                    │ ├─ members       │ ──▶ email_disabled除外
                    │ └─ members_      │
                    │     subscribe_   │ ──▶ 日次増減データ
                    │     events       │
                    └──────────────────┘
                              │
                              ▼
                    ┌──────────────────┐
                    │  累積値計算       │
                    │  日付補完        │
                    └──────────────────┘
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| routes.js | `ghost/core/core/server/web/api/endpoints/admin/routes.js` | ソース | APIルート定義 |
| stats.js | `ghost/core/core/server/api/endpoints/stats.js` | ソース | APIコントローラー |
| stats-service.js | `ghost/core/core/server/services/stats/stats-service.js` | ソース | サービス層ラッパー |
| posts-stats-service.js | `ghost/core/core/server/services/stats/posts-stats-service.js` | ソース | 購読者統計クエリ実装 |
| date-utils.js | `ghost/core/core/server/services/stats/utils/date-utils.js` | ソース | 日付ユーティリティ |
| schema.js | `ghost/core/core/server/data/schema/schema.js` | 設定 | DBスキーマ定義 |
