Appearance
ADM-001 P-WORLDメールアーカイブ管理画面
概要
- P-WORLD 配信メールを IMAP で取得・保存し、管理画面上で一覧・詳細表示・記事リンク付与・削除などを行う管理画面。
- WordPress トップレベルメニューとして登録。操作はすべて admin-ajax 経由の非同期通信で処理する。
- メール取得は 手動実行(AJAX → WP-Cron 非同期)と 定期実行(WP-Cron 単発連鎖)の 2 経路を持つ。
外部インターフェース
管理画面 URL
| 項目 | 値 |
|---|---|
| URL | /wp-admin/admin.php?page=pworld-archive-admin |
| メニュー名 | P-WORLDメールアーカイブ |
| 権限 | manage_options |
admin-ajax(HTTP 契約)
すべて POST /wp-admin/admin-ajax.php、manage_options 権限+操作別 nonce 必須。
action 値 | 説明 |
|---|---|
pworld_archive_run | 手動メール取得ジョブを非同期起動 |
pworld_archive_job_status | 実行中ジョブの状態確認 |
pworld_archive_mail_list | メール一覧取得(20件/ページ) |
pworld_archive_mail_detail | メール詳細取得 |
pworld_mail_delete | メール削除 |
pworld_mail_update_article_link | 記事リンク個別更新 |
pworld_mail_bulk_save_article_links | 記事リンク一括保存 |
pworld_archive_force_reset | 実行中ジョブの強制リセット |
pworld_archive_cron_settings_save | 定期 Cron 設定の保存 |
pworld_gif_still_regenerate | GIF スチルフレーム再生成 |
pworld_gif_backfill_count | GIF バックフィル対象件数取得 |
pworld_gif_backfill | GIF バックフィル実行 |
pworld_gif_backfill_candidates | GIF バックフィル候補一覧取得 |
pworld_mail_deletable_image_count | 削除可能画像数取得 |
POST パラメータ・レスポンス data の契約は EP-一覧 および admin-ajax/endpoints/ を参照。使用画面 ID は ADM-一覧 を参照。
WP-Cron フック
| フック名 | クラス | 実行契機 | 処理内容 |
|---|---|---|---|
pworld_archive_async_run | PworldArchiveAsyncRunner | 手動取得ボタン押下後に WP-Cron 単発登録 | IMAP 取得(デフォルト 5 件、最大 500 件)。MySQL ロックで重複防止 |
pworld_archive_scheduled_job | PworldArchiveScheduledCronRunner | 毎日 21:00 / 22:00 / 22:30(単発連鎖方式) | IMAP 取得 + メール TTL パージ + GIF TTL パージ(各設定で有効化) |
定期 Cron スロット設定(PworldArchiveCronScheduler)
- スロット:
2100,2200,2230(HHmm 形式) daily再発火ではなく単発連鎖(実行後に次回スロットを再登録)- 管理者ログイン時の
admin_initフックでsync_scheduleを 1 日 1 回実行(transient でスロットル) - 取得または TTL パージの少なくともどちらかが有効な場合のみスケジュール登録
サムネイル自動生成(PworldHallDayThumbnailAutoGenerateService)
処理フロー
PworldArchiveService::run() でのメール保存成功直後に呼び出される。
| 呼び出し元 | サービス | タイミング |
|---|---|---|
PworldArchiveAsyncRunner / PworldArchiveScheduledCronRunner → PworldArchiveService::run() | PworldHallDayThumbnailAutoGenerateService | メール 1 件保存成功直後(mail_id, article_target_date, hall_name を渡す) |
入力
| フィールド | 型 | 出所 |
|---|---|---|
mail_id | int | 保存済みメール ID |
article_target_date | string|null | PworldMailArchiveEntity::get_article_target_date() |
hall_name | string | PworldMailArchiveEntity::get_hall_name() |
スキップ条件(早期リターン)
| 条件 | 理由 |
|---|---|
article_target_date が null | 日付未解決のため対象外 |
HallEnum::from_string(hall_name) === null | unknown・未登録ホール名を含む全未解決ホールをスキップ |
トリガーメールの show_in_article = false | 記事表示対象外のメールは処理しない |
対象メールの絞り込み
スキップ条件をパスした後、pworld_mail_archive から同一 article_target_date × hall_name(HallEnum::value 正規化後)かつ show_in_article = true のメールを id ASC で全件取得する。
| 件数 | 処理 |
|---|---|
| 0 件 | スキップ(安全のため) |
| 1 件 | 単体サムネイル生成 |
| 2 件以上 | 先頭 2 件で合成サムネイル生成(3 件目以降は無視) |
各メールの attachment_id 解決
対象メール 1 件ごとに以下の優先順で attachment_id を解決する。メールに pworld_mail_image_ref が 1 件もない場合はそのメールをスキップ扱いとし、解決可能なメールが 0 件なら全体をスキップ。
方針: メイン画像をソースとする
日別ホールサムネの自動生成では、メール本文に複数の画像がある場合でも、告知の 主たるビジュアル(メイン画像) に相当する 1 枚の添付をサムネイルのソースとする。選定はサーバー側で行い、メール HTML をブラウザ表示したときのスクロール位置をシステムが再現してキャプチャする処理は行わない。
旧設計文書にあった「メール本文を縦方向に 100px スクロールした状態で見える GIF」という表現は、運用上「本文を少し下にスクロールしてから手動キャプチャしやすい領域」に近い結果を得たいという意図のたとえであった。自動生成ではそれを メイン画像のヒューリスティック選定(下記)で近似する。
メイン画像の定義(PworldHallDayThumbnailAutoGenerateService 準拠)
複数の pworld_mail_image_ref がある通常メールでは、次の手順で メイン画像に使う ref を決める。
- 当該
mail_idのpworld_mail_image_refをid昇順に列挙する(メール HTML 上の画像登録順に相当)。 - 先頭から順に見て、添付のピクセル寸法が取得できる場合は 帯状の装飾画像とみなしてスキップする条件を適用する:幅 ÷ 高さ ≥ 2.0 かつ 高さ ≤ 200px(PR #2000 で実 P-WORLD バナー 480×193 例を帯と判定するため見直し)。
- スキップされなかった 最初の ref をメイン画像とする。走査中に寸法が取得できない ref に当たった場合は その ref をメイン画像として採用する(以降の帯判定は行わない)。すべて帯判定で消えた場合は 先頭 ref にフォールバックする。
GIF をソースにする場合(画像1枚メールを除く)
メイン画像として選んだ attachment_id が GIF のとき、約 3 秒再生相当の代表静止画(EP-009 経由で生成済みの _pworld_gif_still_attachment_id)があれば、その静止画の attachment_id をサムネイル生成のソースとする。静止画がない場合は 元の GIF 添付をソースとする。
例外(「画像1枚メール」)
P-WORLD 配信メールのうち、Gmail 表示相当のラッパー付きで 本文の主たる視覚コンテンツが 1 枚の画像のみである構成(挨拶行 Dear ... と mailto → <hr> → 単一の <div> 内にメイン img 1 つ → 注記・店舗名・フッターリンク等)に一致するものを「画像1枚メール」とする。実装では processed_html を正とし、次をすべて満たすときに該当する。
img要素が ドキュメント全体で 1 つのみ(トラッキング用など追加imgが混入した場合は本例外に該当しない)。- 当該
imgのsrcが ローカルアップロード配下の URL(取り込み後の正規化済み HTML)であること。 pworld_mail_image_refが当該mail_idで 1 行のみであること。
該当時は GIF 代表静止画への置き換えを行わず、当該 pworld_mail_image_ref の attachment_id(元画像そのもの)をサムネイル元とする。
| 優先 | 条件 | 使用する attachment_id |
|---|---|---|
| 1 | **「画像1枚メール」**の判定を満たす | 当該 ref の attachment_id(GIF でも静止画に差し替えない) |
| 2 | 上記以外(複数画像を含む通常メール。メイン画像は上記 メイン画像の定義 で選んだ ref) | 選んだ ref の attachment_id を起点とし、post_mime_type = image/gif かつ get_post_meta(attachment_id, '_pworld_gif_still_attachment_id') > 0 ならその静止画、否则同 attachment_id |
サムネイル生成(1 件 vs 2 件)
メール由来の解決済み attachment_id(ソース)を pworld_hall_day_thumbnail に直接指させない。単体・合成のいずれも、新規メディアとしてサムネイル用ファイルを生成し、その新 attachment_id を upsert する(ソース添付は変更しない)。
1 件の場合(単体サムネイル生成)
| ステップ | 内容 |
|---|---|
| 1 | 上記解決ロジックで得たソース attachment_id の実ファイルを wp_get_image_editor で読み込む |
| 2 | 固定キャンバス(818x722)へ contain 方式で中央配置する。アスペクト比を維持したまま、縦または横のいずれか一辺がキャンバス上限に達するまで拡大・縮小する |
| 3 | 余白は processed_html から抽出した背景色で塗りつぶす(style の background-color、または bgcolor を優先)。抽出不能時は白(#ffffff)にフォールバックする |
| 4 | ピクセル加工が不要でも、再エンコードして新規ファイルとして保存する(メール添付とサムネイルメディアを分離する) |
| 5 | pworld-thumbnail-single-{mail_id}.jpg として WordPress メディアに保存し新 attachment_id を得る |
| 6 | メディア保存が完全に成功し有効な attachment_id が確定した直後のみ、新 attachment に自動生成マーカー post meta を付与(後述)。失敗時はマーカーを付けない |
2 件の場合(合成画像生成)
| ステップ | 内容 |
|---|---|
| 1 | 解決した 2 つの attachment_id(attachment_a:先行メール、attachment_b:後行メール)の実ファイルを wp_get_image_editor で読み込む |
| 2 | 両画像を同一サイズに統一する(小さい方の高さ基準でリサイズ) |
| 3 | 横並び合成:左に attachment_a、右に attachment_b を配置 |
| 4 | pworld-composite-{mail_id_a}-{mail_id_b}.{ext} として WordPress メディアに保存し新 attachment_id を得る |
| 5 | メディア保存が完全に成功し有効な attachment_id が確定した直後のみ、新 attachment に自動生成マーカー post meta を付与(後述)。失敗時はマーカーを付けない |
単体・合成のいずれかで生成に失敗した場合は、生成途中の一時ファイルは wp_delete_file 等で削除し、attachment は作成しない・マーカーは付与しない(下表)。
サムネイル生成失敗時のエラー処理(単体・合成共通)
リトライは行わない。pworld_hall_day_thumbnail にセンチネル値は置かない。
| 失敗箇所 | 挙動 | ログ | pworld_hall_day_thumbnail |
|---|---|---|---|
wp_get_image_editor が false / WP_Error | 当該実行の生成処理を中止 | error_log、先頭タグ例 [PworldHallDayThumbnailAutoGenerate]、単体: mail_id・ソース attachment_id / 合成: mail_id_a・mail_id_b・各ソース attachment_id、失敗理由 | 変更しない |
リサイズ・合成(resize / crop / ピクセル合成など)失敗 | 同上(単体経路でピクセル加工が無い場合は該当しにくい) | 同上 | 変更しない |
メディア保存失敗(ファイル書き込み失敗、wp_insert_attachment 等失敗) | 生成済みの一時ファイルは wp_delete_file 等で削除。attachment は作成しない | 同上 | 変更しない |
生成失敗時は旧 attachment_id も削除しない(既存サムネイルを維持する)。
旧サムネイル削除(自動生成分のみ)
upsert の前に、同一 article_target_date × hall_name の既存行があれば旧 attachment_id を退避する。新サムネイルの生成が成功し(単体・合成ともにメディア保存まで完了)、pworld_hall_day_thumbnail の upsert が成功した後に限り、退避した旧 attachment_id を下表で評価して削除する。生成失敗・メディア保存失敗・upsert 失敗のいずれかでは旧 attachment は削除しない。
| 確認内容 | 処理 |
|---|---|
退避した旧 attachment_id が存在する | 下記 meta を確認 |
get_post_meta(旧_attachment_id, '_pworld_hall_day_thumbnail_auto_generated') === '1' | wp_delete_attachment(旧_attachment_id, true) で物理削除 |
| 上記 meta がない(手動登録画像) | attachment は削除しない(upsert でレコードのみ上書き) |
自動生成マーカー(WordPress post meta)
DB スキーマ変更なし。単体・合成で新規作成したサムネイル attachment について、メディア保存が完全に成功し有効な attachment_id が確定した直後にのみ _pworld_hall_day_thumbnail_auto_generated を付与する。保存失敗時は付与しない。メール由来のソース attachment には付与しない。
| meta キー | 値 | 説明 |
|---|---|---|
_pworld_hall_day_thumbnail_auto_generated | '1' | 自動生成フラグ(手動登録画像と区別) |
pworld_hall_day_thumbnail への upsert
| カラム | 値 |
|---|---|
article_target_date | 入力の article_target_date |
hall_name | HallEnum::from_string(hall_name)->value(正規化済みスラッグ) |
attachment_id | 新規生成したサムネイル用 attachment_id(単体: pworld-thumbnail-single-*、合成: pworld-composite-*) |
| ON DUPLICATE KEY UPDATE | attachment_id を上書き |
hall_name の unknown 判定
HallEnum::from_string()はisland/espasu/bigapple/unoのみ非 null を返す- それ以外(
unknown・空文字・未登録ホール名)はすべて null → スキップ - 登録する
hall_nameはHallEnum::value(正規化済みスラッグ)を使用し、生文字列は使わない
エラー
| 条件 | ユーザー向け挙動 | メッセージ / ログ |
|---|---|---|
| admin-ajax で nonce 不正 | 400 / エラーメッセージ返却 | wp_die() または JSON error レスポンス |
| 手動取得時のクールダウン中(1分以内の再実行) | ジョブ起動せず、クールダウン中メッセージ | pworld_archive_cooldown transient で判定 |
| 非同期ジョブで MySQL ロック取得失敗 | ジョブ状態は running のまま維持 | error_log([PworldArchiveAsyncRunner] 先頭) |
非同期ジョブで \Throwable | ジョブ状態を error に更新 | error_log([PworldArchiveAsyncRunner] 先頭) |
| 定期 Cron でロック取得失敗 | スキップ(ログに skipped/lock_not_acquired) | PworldArchiveScheduledJobLogger::log() に記録 |
定期 Cron で \Throwable | ログに error として記録 | PworldArchiveScheduledJobLogger::log() に status: error |