築心相簿架構文件 v1.3 Whitepaper
版本:1.3.0 更新日期:2026-10-07 架構:Zero-Framework PHP + SQLite WAL

築心相簿 (Chuhsin Photos)
系統架構與技術白皮書

一套專為小型非營利組織量身打造、具備 Google Photos 級流暢體驗的輕量級 Web 相簿系統。本文件詳細記錄其雙軌 WebP 影像管線、安全隔離模型、資料庫調校、拖曳排序與容量規劃原理。

1.1 執行摘要與系統願景

在傳統相簿系統中,小型組織經常面臨以下結構性難題:

  1. 主機空間爆炸:志工以現代智慧型手機拍攝(單張原檔 8MB~15MB),未經妥善壓縮即直接存放,迅速灌滿 10GB 虛擬主機。
  2. 行動網路載入延遲:未經多尺度縮圖優化,手機開啟相簿需消耗數十 MB 流量下載原始檔案,導致畫面卡頓。
  3. EXIF 方向旋轉錯亂:iPhone 與相機在直幅拍攝時依賴 EXIF Orientation 標記,在未校正的網頁中常呈現倒置或旋轉 90 度。
  4. 協作投稿安全風險:若在公開瀏覽頁面直接開放上傳,容易遭遇未受邀民眾誤傳或網路爬蟲惡意灌水。
  5. 檔案遺留與孤兒膨脹:後台刪除相簿或照片時,實體磁碟常殘留垃圾檔案,造成儲存黑洞。

築心相簿採用「零原始圖檔儲存(Zero-Original Retention)」、「全規格自動旋轉 WebP 管線」與「專屬 Token 隔離投稿路由」,徹底根治上述痛點,達成以極低運算資源實現企業級流暢度與高安全維運之目標。

1.2 技術選型與環境拓撲

為確保可在標準 Linux / Synology NAS Web Station 或低成本虛擬主機上實現零維護、免 Node.js、免大型框架、隨裝即跑,技術堆疊嚴格選用:

組件維度 選用技術 選型理由與優勢
後端核心 PHP 8.2+ (原生 Strict Types) 執行速度極快,記憶體開銷小,無需 npm build 或長駐 Node 程序。
關聯資料庫 SQLite 3 (WAL Mode + PDO) 單一檔案儲存、備份極簡,開啟 WAL 與記憶體快取後並行讀取效能超越 MySQL。
影像引擎 PHP GD + ImageMagick (Imagick) GD 處理超低開銷的 JPEG/WebP 壓縮與 EXIF 校正;Imagick 解析 HEIC/HEIF。
QR Code 模組 QRCodeService (原生純 PHP) 無任何外部 Composer 依賴,直接生成乾淨向量 SVG 與高品質 PNG 串流,即時提供活動掃碼投稿。
壓縮封裝 PHP ZipArchive 無額外相依,直接在伺服器端將 WebP 即時轉碼為高品質 JPEG 串流打包下載。
前端架構 Vanilla JavaScript + 原生 CSS 不載入 Vue/React 等龐大函式庫,首屏傳輸體積小於 60KB,啟動時間小於 100ms。
行動應用 PWA (Service Worker v1.1.2 + Manifest) 支援離線靜態資源快取、可安裝至 iPhone/Android 手機主畫面以 App 獨立視窗啟動。

2.1 影像處理管線架構

上傳至築心相簿的每一張相片,均嚴格遵循以下確定性流水線(Pipeline):

1. 安全過濾與探測
二進位 MIME 檢查,相機照片白名單(JPEG/HEIC/WebP),拒絕 PNG/GIF/BMP/TIFF/SVG 及影片
➔
2. 影像解碼與旋轉
HEIC 經 Imagick、JPEG 經 GD,精準解析 EXIF 1~8 標籤完成旋轉並抹除隱私 GPS
⬇
3. 雙規格 WebP 編碼
產出 Full (1200px, Q78) 與 Thumb (400px, Q72),不放大原始尺寸
➔
4. 抹除原檔 & DB 寫入
@unlink 刪除暫存原檔,SQLite 交易原子性寫入,記憶體即刻 imagedestroy

2.2 EXIF 1~8 全規格旋轉校正演算法

手機拍照時,陀螺儀將拍攝角度寫入 EXIF Orientation 標籤(值為 1 至 8)。多數網頁瀏覽器解碼影像時忽略此標籤導致照片旋轉倒置。ImageService::fixOrientation 完整實作了 8 種幾何轉換:

switch ($orientation) {
    case 2: imageflip($image, IMG_FLIP_HORIZONTAL); break;
    case 3: $image = imagerotate($image, 180, 0); break;
    case 4: imageflip($image, IMG_FLIP_VERTICAL); break;
    case 5: imageflip($image, IMG_FLIP_HORIZONTAL); $image = imagerotate($image, 90, 0); break;
    case 6: $image = imagerotate($image, -90, 0); break; // 順時針 90°
    case 7: imageflip($image, IMG_FLIP_HORIZONTAL); $image = imagerotate($image, -90, 0); break;
    case 8: $image = imagerotate($image, 90, 0); break;  // 順時針 270°
}

2.3 雙解析度 WebP 編碼參數最佳化

系統經過嚴密的視覺感官與壓縮率實測(SSIM & PSNR 評估),確立了雙尺寸規範:

  • Full Image:長邊上限 1200px,WebP 品質設定 78。相較於原檔 10MB JPEG,壓縮後僅約 100KB~180KB(壓縮率達 98.5%),視覺細節無肉眼可察覺之損失。
  • Thumb Image:長邊上限 400px,WebP 品質設定 72。單張縮圖體積僅約 15KB~35KB,專門供應相簿瀑布流格狀載入。
  • 禁止放大(No Upscaling):若上傳相片原始尺寸小於上限,維持原尺寸編碼,避免模糊插值。

2.4 零原始圖檔殘留原則 (Zero-Original Retention)

磁碟極簡防禦核心:
伺服器在 ImageService::processUploadedPhoto() 產出 Full 及 Thumb WebP 之後,立即以 @unlink($file['tmp_name']) 抹除 PHP 暫存原始圖檔。整個系統的磁碟空間永遠不儲存任何原始未壓縮 JPEG 或 HEIC 檔案。此舉徹底避免磁碟被相機原檔撐爆。

2.5 即時轉碼 ZIP 下載管線

雖然伺服器全數儲存 WebP,但為讓志工或民眾匯出後能直接使用於 Office 軟體、社群刊登或實體照片沖印,ImageService::exportAlbumAsZip 實作了「即時動態轉碼為 JPEG 串流封裝」:

  1. 透過 imagecreatefromwebp() 讀取 1200px Full WebP。
  2. 建立 TrueColor 畫布並填補白色背景(杜絕潛在透明度黑邊),調用 imagejpeg($canvas, null, 90) 產出高品質 JPEG 位元組流。
  3. 使用 ZipArchive::addFromString("001_名稱.jpg", $data) 寫入臨時 ZIP,並在每次迭代立即 imagedestroy() 與 unset(),將記憶體佔用壓制在 32MB 以內。
  4. 輸出標準 Content-Type: application/zip 與符合 RFC 5987 之 filename*=UTF-8'' 中文檔名,完成後自動刪除臨時 ZIP 檔案。

2.6 檔案組織架構與 URL 路由隔離

為避免上千張照片堆積在單一目錄造成檔案系統效能劣化,系統採用「年份 / 月日 / 相簿代稱」三層階層式目錄隔離儲存:

photos/uploads/
├── full/
│ └── 2026/
│ └── 12-25/
│ └── xmas-party/ <-- yyyy / mm-dd / URL slug
│ ├── 799667ce_full.webp
│ └── 05db1368_full.webp
└── thumbs/
└── 2026/
└── 12-25/
└── xmas-party/ <-- yyyy / mm-dd / URL slug
├── 799667ce_thumb.webp
└── 05db1368_thumb.webp
  • 相簿公開檢視路由:/albums/{slug}/(或 index.php?album={slug})。公開瀏覽頁面採用完全匿名隱私設計,絕不顯示任何投稿按鈕或透露開放投稿資訊,維護公眾瀏覽的純淨感。
  • 專屬訪客投稿路由:/upload/{token}/(或 upload/?token={token})。僅持有專屬加密密鑰 Token 或掃描專屬 QR Code 的活動工作人員與受邀者方可開啟投稿介面。
  • 自動相簿重命名遷移:若管理員修改相簿日期或 Slug,系統會自動移動實體磁碟目錄並同步更新資料庫記錄。
  • 自動垃圾空目錄回收:照片或相簿刪除時,系統會自動遞迴向上回收已無檔案的空資料夾。

3.1 SQLite 實體關聯模型 (ER Model)

資料庫採用單一實體檔案(data/photos.sqlite),核心資料表具備嚴謹的外鍵關聯與級聯刪除:

-- 1. 相簿資料表 (Albums)
CREATE TABLE albums (
    id                      INTEGER PRIMARY KEY AUTOINCREMENT,
    title                   TEXT NOT NULL,
    slug                    TEXT NOT NULL UNIQUE,
    description             TEXT NOT NULL DEFAULT '',
    album_date              TEXT NOT NULL,
    cover_photo_id          INTEGER NULL,
    is_public               INTEGER NOT NULL DEFAULT 1,
    guest_upload_enabled    INTEGER NOT NULL DEFAULT 0,
    guest_upload_token      TEXT NULL UNIQUE,
    guest_upload_expires_at TEXT NULL,
    guest_upload_until      TEXT NULL,
    sort_order              INTEGER NOT NULL DEFAULT 0,
    created_at              TEXT NOT NULL DEFAULT (datetime('now', '+8 hours')),
    updated_at              TEXT NOT NULL DEFAULT (datetime('now', '+8 hours'))
);

-- 2. 相片資料表 (Photos)
CREATE TABLE photos (
    id                      INTEGER PRIMARY KEY AUTOINCREMENT,
    album_id                INTEGER NOT NULL,
    filename                TEXT NOT NULL,
    thumb_filename          TEXT NOT NULL,
    original_filename       TEXT NOT NULL,
    mime_type               TEXT NOT NULL DEFAULT 'image/webp',
    width                   INTEGER NOT NULL DEFAULT 0,
    height                  INTEGER NOT NULL DEFAULT 0,
    file_size               INTEGER NOT NULL DEFAULT 0,
    uploaded_by             TEXT NOT NULL DEFAULT '',
    upload_source           TEXT NOT NULL DEFAULT 'admin',
    sort_order              INTEGER NOT NULL DEFAULT 0,
    created_at              TEXT NOT NULL DEFAULT (datetime('now', '+8 hours')),
    FOREIGN KEY (album_id) REFERENCES albums(id) ON DELETE CASCADE
);

-- 3. 管理員使用者表 (Users)
CREATE TABLE users (
    id                      INTEGER PRIMARY KEY AUTOINCREMENT,
    username                TEXT NOT NULL UNIQUE,
    password                TEXT NOT NULL,
    display_name            TEXT NOT NULL DEFAULT '系統管理員',
    created_at              TEXT NOT NULL DEFAULT (datetime('now', '+8 hours'))
);

-- 效能索引
CREATE UNIQUE INDEX IF NOT EXISTS idx_albums_slug ON albums(slug);
CREATE UNIQUE INDEX IF NOT EXISTS idx_albums_token ON albums(guest_upload_token);
CREATE INDEX IF NOT EXISTS idx_albums_date ON albums(album_date DESC);
CREATE INDEX IF NOT EXISTS idx_albums_sort ON albums(sort_order ASC, album_date DESC);
CREATE INDEX IF NOT EXISTS idx_photos_album ON photos(album_id, sort_order ASC, id DESC);
CREATE INDEX IF NOT EXISTS idx_photos_created ON photos(created_at DESC);
ℹ️
網址代稱 (Slug) 規格約束:
為確保靜態網址相容性與避免中文 URL 編碼亂碼,slug 欄位強制限制僅能由英文字母 (a-z, A-Z)、數字 (0-9) 與連字號 (-) 組成,長度介於 1~64 字元,並排除系統保留字(如 admin, api, upload, albums, uploads 等)。

3.2 WAL 並行高吞吐調校 (PRAGMA Tuning)

為避免 SQLite 在多人同時上傳與瀏覽時產生 database is locked 鎖定等待,資料庫連線初始化執行以下 PRAGMA 指令:

  • PRAGMA journal_mode = WAL;:啟用預寫式日誌(Write-Ahead Logging),實現讀寫分離,讀取者完全不阻擋寫入者,寫入者亦不阻擋讀取者。
  • PRAGMA synchronous = NORMAL;:在 WAL 模式下維持完整資料完整性的同時,減少磁碟 fsync 頻率,寫入吞吐量提升 3~5 倍。
  • PRAGMA busy_timeout = 5000;:並行寫入競爭時自動等待 5000ms 重試,杜絕瞬間鎖定報錯。
  • PRAGMA temp_store = MEMORY;:臨時表與排序運算全數在記憶體進行。

4.1 認證與 Session 防護

管理員密碼採用現代不可逆演算法 PASSWORD_BCRYPT(Cost 10)加鹽雜湊。Session 控制機制遵循 OWASP 安全指引:

  • HttpOnly:Cookie 禁止透過 JavaScript 存取,杜絕 XSS 竊取憑證。
  • SameSite=Lax 與 Secure:防範跨站請求偽造(CSRF)。
  • 雙重 CSRF Token:所有寫入性 API(POST)均需驗證隨機產生的 64 位元 Hex CSRF Token。

4.2 GD 防機器人驗證碼與即時預檢 (CaptchaService)

訪客上傳採用「雙階段校驗架構」,兼顧即時使用者體驗與後端絕對安全性:

  • 即時預檢機制(Real-time Pre-check):前端輸入 4 位數驗證碼後,即時觸發 api.php?action=captcha-check 呼叫 CaptchaService::validateWithoutDestroy($code)。此階段不銷毀 Session 驗證碼,提供前端即時視覺打勾回饋,並配合授權聲明勾選以解鎖相片選取器。
  • 最終一次性驗證(Single-Use Verification):在點擊提交上傳時,後端呼叫 CaptchaService::verify($code),比對成功立即自 $_SESSION 銷毀,徹底杜絕重放攻擊(Anti-Replay Attack)。
  • 字元庫優化:排除易混淆字元(如 0/O, 1/I/L),保留高辨識度之 30 個大寫字母與數字。
  • 動態干擾繪圖:繪製隨機干擾弧線、高密度噪點像素,並隨機旋轉與字型垂直位移,阻絕 OCR 辨識。

4.3 多層級檔案安全審計與隔離

系統不依賴前端傳入的 MIME 類型,在後端進行三道深度防禦:

  1. 二進位魔術字節審計:調用 PHP finfo(FILEINFO_MIME_TYPE) 讀取實體檔案檔頭。
  2. ISOBMFF 特徵識別:對於 iPhone HEIC 檔案,讀取前 16 位元組檢查 ftypheic / ftypmif1 特徵。
  3. 嚴格格式白名單政策:僅允許相機拍照格式(image/jpeg、image/heic、image/webp,單張上限 15MB);嚴格拒絕非壓縮或向量格式(PNG、GIF、BMP、TIFF、SVG)以及所有影片格式(MP4、MOV 等)。
  4. 強制相片授權聲明:必須先勾選【相片授權使用與無侵權聲明】,前後端嚴格雙重比對,否則拒絕處理上傳。

4.4 權限隔離與防止代碼執行 (.htaccess)

在 Apache 伺服器環境下,針對敏感目錄實施權限封鎖:

  • data/.htaccess:直接以 Require all denied 徹底禁止外部瀏覽器讀取 photos.sqlite 資料庫檔。
  • uploads/.htaccess:使用 php_flag engine off 關閉 PHP 解析引擎,並禁止存取 .php、.sh、.phtml 檔案,防止上傳偽裝的木馬腳本執行。

5.1 10GB 容量推演模型

在組織分配的 10GB(10,240MB)主機空間限制下,容量推演如下:

規格項目 平均大小 可容納數量 (10GB) 備註
Full WebP (1200px) 120 KB 約 85,000 張 高品質大圖展示
Thumb WebP (400px) 25 KB 約 400,000 張 首頁瀑布流載入
綜合組合 (Full + Thumb) 145 KB / 張 約 65,000~70,000 張 若一年舉辦 100 場活動,每場 100 張,足以連續使用 7 年以上

5.2 100 張相片與 512MB 相簿硬性配額

為避免特定熱門活動無節制上傳擠佔主機,系統在 ImageService 與 api.php 實施強制配額規則:

  • 張數上限:單本相簿最高限制 100 張相片(登入管理員與訪客上傳皆適用)。上傳前與寫入中均進行精準截斷。
  • 容量上限:單本相簿總容量最高限制 512 MB(登入管理員與訪客上傳皆適用)。
  • 單張大小:單張照片最大上限 15 MB。

5.3 孤兒檔案掃描與 VACUUM 回收演算法

網路斷線或非預期關閉瀏覽器可能導致暫存檔案殘留在磁碟。ImageService::scanOrphans 與 purgeOrphans:

  1. 讀取 SQLite photos 取得現存有效檔名清單,建立 O(1) 雜湊對應表。
  2. 遞迴遍歷 uploads/full 與 uploads/thumbs,自動過濾 Mac WebDAV 特殊隱藏檔(如 ._*)。
  3. 比對找出不在資料庫內的孤兒檔案,並在管理員授權下一鍵刪除。
  4. 清理完畢後自動觸發 VACUUM 命令,壓縮 SQLite 資料庫檔案釋回磁區。

6.1 RESTful API 規格清單

系統全部 API 均以 api.php?action={action} 提供,返回統一結構之 JSON:

Action Method 權限 功能說明
auth-status GET 公開 取得目前使用者登入狀態、CSRF Token、相簿配額限制常數。
login POST 公開 管理員帳密驗證登入。
logout POST 登入 銷毀 Session 安全登出。
change-password POST 管理員 驗證舊密碼並以 BCrypt (Cost 10) 演算法更新管理員登入密碼。
albums GET 公開 取得相簿列表(支援 sort_order 自訂排序與日期倒序)。
albums-reorder POST 管理員 批次更新首頁相簿卡片之自訂拖曳排列順序。
album GET 公開 依 id 或 slug 取得單一相簿中繼資料與照片分頁列表。
album-create POST 管理員 建立新相簿(標題、slug、日期、訪客投稿開關/期限、自動生成 Token 等)。
album-update-title POST 管理員 快速更新相簿標題。
album-update POST 管理員 完整更新相簿資料(標題、Slug、日期、描述、公開性、訪客截止時間等)。
album-regenerate-token POST 管理員 重新產生相簿專屬訪客投稿 Secret Token 與 QR Code。
upload-info GET 公開 (Token) 依專屬 Token 取得投稿頁面所需之相簿標題、日期、剩餘配額與截止時間。
album-delete POST 管理員 永久刪除相簿(若有相片需傳入 confirm_title 驗證)。
album-set-cover POST 管理員 設定特定照片為相簿封面。
album-download-zip GET 公開/權限 將整本相簿照片即時轉為 JPEG 並打包成 ZIP 串流下載。
photo-upload POST 訪客/管理員 批次上傳相片,執行白名單審計、EXIF 校正、雙 WebP 轉碼與配額防護。
photos-reorder POST 管理員 批次更新相簿內照片自訂順序(支援拖曳排序與前後移動)。
photo-delete POST 管理員 永久刪除單張相片,並同步重設或解除封面指派。
captcha GET 公開 輸出 4 位防機器人圖形驗證碼(PNG 串流)。
captcha-check GET/POST 公開 即時預檢驗證碼正確性(不銷毀 Session,供前端即時解鎖上傳器)。
qrcode GET 公開 生成指定文字或網址之 QR Code 圖片(支援 PNG/SVG 格式輸出)。
orphan-scan GET 管理員 遞迴掃描磁碟中未被資料庫記錄之孤兒垃圾檔案。
orphan-purge POST 管理員 一鍵刪除孤兒檔案並執行 SQLite VACUUM 壓縮。
storage-stats GET 管理員 取得目前伺服器磁碟用量與相片統計資訊。