iT邦幫忙

2026 iThome 鐵人賽

DAY 21
0

在前一天的內容中,完成後端還書程式碼return.php。

但在測試這套還書功能時,有一個容易忽視、卻會直接影響商業邏輯的細節——伺服器與專案時區不一致。

今天來聊聊因為時區算錯天數,並實作 「我的借閱紀錄(my_loans.php)」 介面,讓使用者可以順暢查看借書狀況並點擊還書。

一、為什麼凌晨還書會被多算一天?

在沒有特別設定時,PHP 與 MySQL 的預設時區通常是 UTC(協調世界時)。而台灣的時區是 UTC+8(Asia/Taipei)。

這會造成什麼問題?
假設使用者在 10 月 5 日凌晨 01:30 打開網頁按下還書:

  • 台灣時間(使用者認知):2026-10-05 01:30:00(若應還日期是 2026-10-04,只逾期 1 天)。
  • 伺服器時間(UTC):2026-10-04 17:30:00(日期竟然還卡在 10-04,這會導致逾期天數少算一天,甚至判斷成沒逾期)。

相反地,如果資料庫存的是 UTC,但程式用台灣時間去減,也很容易出現天數計算偏移的怪現象。

為了解決時區混亂,在 PHP 處理日期比對時做了兩件極其關鍵的事:

  1. 明確指定時區物件:
    // PHP 預設時區是 UTC,比台灣慢 8 小時,凌晨還書會被算成前一天,所以明確指定 Asia/Taipei
    $timezone = new DateTimeZone('Asia/Taipei');
    $now = new DateTime('now', $timezone); // 目前時間,稍後寫進 returned_at
    $today = new DateTime('today', $timezone); // 'today' 是今天 00:00:00,只比日期不比時分秒
    
  2. 使用 ! 強制重置時分秒:
    如果不加 !,PHP 可能會把當前時間的時分秒帶入 $dueDate 中,導致兩個 DateTime 物件在用 diff() 比對時產生微小的時差偏差。
    // createFromFormat() 依指定格式解析字串,開頭的 ! 會把沒給的時分秒歸零,讓 due_date 也是當天 00:00:00
    $dueDate = DateTime::createFromFormat('!Y-m-d', $loan['due_date'], $timezone);
    

二、實作「我的借閱紀錄」頁面my_loans.php

有了穩固的後端與時區處理,現在來建立my_loans.php 頁面,提供使用者閱讀借閱紀錄與進行還書操作。

向 Claude Code 發送 Prompt 協助產生my_loans.php:

請幫我撰寫 my_loans.php 頁面:

1. 檢查 Session 確保會員已登入,並從資料庫查詢該會員的所有借閱紀錄(包含 loans 與 books 的 JOIN 欄位),依 borrowed_at 倒序排列。
2. 顯示全站 Flash Message(提示成功、警告或錯誤訊息)。
3. 使用表格清單呈現在借書籍與歷史歸還紀錄:
    - 若狀態為 borrowed 且已超過 due_date,以醒目的紅色/黃色標籤標示『已逾期』。
    - 若狀態為 borrowed,提供一個 POST 表單按鈕觸發 return.php(需帶入 loan_id)。
4. 使用 Tailwind CSS 進行排版,確保畫面乾淨清楚。
<?php

// ============================================================
// 【PHP】區塊:啟動 Session 與載入資料庫
// ============================================================
// 必須在任何 HTML 輸出之前呼叫 session_start(),之後才能讀寫 $_SESSION
session_start();

require __DIR__ . '/config/database.php';
$pdo = Database::getConnection();

// ============================================================
// 【PHP】區塊:檢查是否已登入
// ============================================================
// 登入成功時 login.php 會寫入 $_SESSION['member_id'],沒有這個值就代表尚未登入
if (!isset($_SESSION['member_id'])) {
    header('Location: login.php');
    exit;
}

$memberId = (int) $_SESSION['member_id'];

// ============================================================
// 【PHP】區塊:查詢會員的所有借閱紀錄
// ============================================================
// JOIN loans 與 books 表,取出借閱者所有借書紀錄(含已借出與已歸還)
// 依 borrowed_at 倒序排列(最新的借閱紀錄優先顯示)
$sql = 'SELECT
            l.id AS loan_id,
            l.member_id,
            l.book_id,
            l.borrowed_at,
            l.due_date,
            l.returned_at,
            l.status,
            b.title,
            b.author,
            b.category
        FROM loans AS l
        JOIN books AS b ON b.id = l.book_id
        WHERE l.member_id = :member_id
        ORDER BY l.borrowed_at DESC';

$stmt = $pdo->prepare($sql);
$stmt->execute([':member_id' => $memberId]);
$loans = $stmt->fetchAll();
// ============================================================
// 【PHP】區塊:讀取 Flash Message(一次性提示訊息)
// ============================================================
// 取出後立刻 unset() 刪除,重新整理頁面就不會重複顯示
$flash = $_SESSION['flash'] ?? null;
unset($_SESSION['flash']);

// ============================================================
// 【PHP】區塊:輔助函式:判斷是否逾期
// ============================================================
// 借閱狀態為 'borrowed' 且目前日期已超過歸還期限,就算逾期
function isOverdue($dueDate, $status): bool
{
    if ($status !== 'borrowed') {
        return false;
    }

    $timezone = new DateTimeZone('Asia/Taipei');
    $today = new DateTime('today', $timezone);
    $due = DateTime::createFromFormat('!Y-m-d', $dueDate, $timezone);

    return $today > $due;
}

?>
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>我的借閱紀錄 - 圖書管理系統</title>
    <script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-gray-100">
    <!-- ============================================================ -->
    <!-- 【HTML】區塊:頁面標題與導航列 -->
    <!-- ============================================================ -->
    <nav class="bg-slate-900 text-white py-4 shadow">
        <div class="container mx-auto px-4 flex justify-between items-center">
            <h1 class="text-2xl font-bold">圖書管理系統</h1>
            <div class="space-x-4">
                <a href="index.php" class="hover:text-gray-300 transition">首頁</a>
                <span class="text-gray-300">|</span>
                <a href="my_loans.php" class="font-semibold text-yellow-300">我的借閱</a>
                <span class="text-gray-300">|</span>
                <a href="logout.php" class="hover:text-gray-300 transition">登出</a>
            </div>
        </div>
    </nav>
    
     <!-- ============================================================ -->
    <!-- 【HTML】區塊:全站 Flash Message(提示訊息) -->
    <!-- ============================================================ -->
    <!-- 借書、還書、登入成功等操作完成時,會透過 session 帶回一個提示訊息,在這裡顯示 -->
    <div class="container mx-auto px-4 py-4">
        <?php if ($flash !== null): ?>
            <?php $isSuccess = $flash['type'] === 'success'; ?>
            <?php $isWarning = $flash['type'] === 'warning'; ?>
            <div class="rounded-lg px-4 py-3 mb-4 <?= $isSuccess ? 'bg-green-100 border border-green-400 text-green-700>
                <?= htmlspecialchars($flash['message']) ?>
            </div>
        <?php endif; ?>
    </div>

    <!-- ============================================================ -->
    <!-- 【HTML】區塊:借閱清單標題與篩選選項卡 -->
    <!-- ============================================================ -->
    <div class="container mx-auto px-4 py-6">
        <h2 class="text-3xl font-bold text-gray-800 mb-6">我的借閱紀錄</h2>
        
       <?php if (empty($loans)): ?>
            <!-- ================================================ -->
            <!-- 【HTML】子區塊:無借閱紀錄時的空狀態提示 -->
            <!-- ================================================ -->
            <div class="bg-white rounded-lg shadow p-8 text-center">
                <p class="text-gray-600 text-lg">目前尚無借閱紀錄</p>
                <a href="index.php" class="inline-block mt-4 bg-slate-900 text-white px-6 py-2 rounded hover:bg-slate-8>
                    前往瀏覽書籍
                </a>
            </div> 
        <?php else: ?>
            <!-- ================================================ -->
            <!-- 【HTML】子區塊:借閱清單表格 -->
            <!-- ================================================ -->
            <!-- 使用響應式表格,在手機上會自動換列顯示 -->
            <div class="overflow-x-auto bg-white rounded-lg shadow">
                <table class="w-full">
                    <!-- 【HTML】表頭:列出各欄位名稱 -->
                    <thead>
                        <tr class="bg-slate-900 text-white">
                            <th class="px-4 py-3 text-left">書名</th>
                            <th class="px-4 py-3 text-left">作者</th>
                            <th class="px-4 py-3 text-left">類別</th>
                            <th class="px-4 py-3 text-left">借閱日期</th>
                            <th class="px-4 py-3 text-center">歸還期限</th>
                            <th class="px-4 py-3 text-center">狀態</th>
                            <th class="px-4 py-3 text-center">操作</th>
                        </tr>
                    </thead>
                    <!-- 【HTML】表身:逐筆顯示借閱紀錄 -->
                    <tbody class="divide-y divide-gray-200">
                        <?php foreach ($loans as $loan): ?>
                            <?php
                                // 判斷是否逾期
                                $overdue = isOverdue($loan['due_date'], $loan['status']);
                                // 判斷是否已歸還
                                $isReturned = $loan['status'] === 'returned';
                            ?>
                            <tr class="hover:bg-gray-50 transition">
                                <!-- 書名(可點擊看詳細) -->
                                <td class="px-4 py-3 font-semibold text-gray-800">
                                    <?= htmlspecialchars($loan['title']) ?>
                                </td>
                                <!-- 作者 -->
                                <td class="px-4 py-3 text-gray-700">
                                    <?= htmlspecialchars($loan['author']) ?>
                                </td>
                                <!-- 類別 -->
                                <td class="px-4 py-3 text-gray-700">
                                    <?= htmlspecialchars($loan['category']) ?>
                                </td>
                                <!-- 借閱日期 -->
                                <td class="px-4 py-3 text-gray-700">
                                    <?= htmlspecialchars(date('Y-m-d H:i', strtotime($loan['borrowed_at']))) ?>
                                </td>
                                <!-- 歸還期限:如果逾期則用紅色標籤突出 -->
                                <td class="px-4 py-3 text-center">
                                    <?php if ($isReturned): ?>
                                        <!-- 已歸還則顯示實際歸還日期 -->
                                        <span class="text-gray-700">
                                            <?= htmlspecialchars(date('Y-m-d', strtotime($loan['returned_at']))) ?>
                                        </span>
                                    <?php else: ?>
                                        <!-- 尚未歸還則顯示期限日期,逾期時用紅色標籤 -->
                                        <?php if ($overdue): ?>
                                            <span class="inline-block bg-red-500 text-white px-3 py-1 rounded-full text>
                                                已逾期 · <?= htmlspecialchars($loan['due_date']) ?>
                                            </span>
                                        <?php else: ?>
                                            <span class="text-gray-700">
                                                <?= htmlspecialchars($loan['due_date']) ?>
                                            </span>
                                        <?php endif; ?>
                                    <?php endif; ?>
                                </td>
                                <!-- 狀態:借出中 / 已歸還 -->
                                <td class="px-4 py-3 text-center">
                                    <?php if ($isReturned): ?>
                                        <!-- 已歸還的狀態標籤:灰色 -->
                                        <span class="inline-block bg-gray-300 text-gray-800 px-3 py-1 rounded-full text>
                                            已歸還
                                        </span>
                                    <?php else: ?>
                                        <!-- 借出中的狀態標籤:藍色;逾期時改為紅色 -->
                                        <span class="inline-block <?= $overdue ? 'bg-red-200 text-red-800' : 'bg-blue-2>
                                            <?= $overdue ? '逾期中' : '借出中' ?>
                                        </span>
                                    <?php endif; ?>
                                </td>
                                 <!-- 操作按鈕:借出中才能歸還;已歸還則無按鈕 -->
                                <td class="px-4 py-3 text-center">
                                    <?php if (!$isReturned): ?>
                                        <!-- ========================================= -->
                                        <!-- 【HTML】還書表單:POST 表單觸發 return.php -->
                                        <!-- ========================================= -->
                                        <!-- 只有借出中的書籍才能歸還,表單送出 loan_id 給 return.php 處理 -->
                                        <form method="POST" action="return.php" class="inline">
                                            <!-- 【HTML】隱藏欄位:存放 loan_id,提交表單時會一起送出 -->
                                            <input type="hidden" name="loan_id" value="<?= htmlspecialchars($loan['loan>
                                            <!-- 【HTML】提交按鈕:依逾期與否改變按鈕顏色 -->
                                            <button type="submit" class="<?= $overdue ? 'bg-red-500 hover:bg-red-600' :>
                                                <?= $overdue ? '逾期歸還' : '歸還' ?>
                                            </button>
                                        </form>
                                    <?php else: ?>
                                        <!-- 已歸還則不顯示按鈕 -->
                                        <span class="text-gray-400">—</span>
                                    <?php endif; ?>
                                </td>
                            </tr>
                        <?php endforeach; ?>
                    </tbody>
                </table>
            </div>
        <?php endif; ?>
    </div>

    <!-- ============================================================ -->
    <!-- 【HTML】區塊:頁腳 -->
    <!-- ============================================================ -->
    <footer class="bg-slate-900 text-white text-center py-4 mt-12">
        <p class="text-sm">© 2024 圖書管理系統 - 版權所有</p>
    </footer>
</body>
</html>

三、瀏覽器驗證與操作測試

頁面拉好後,就能打開瀏覽器對昨天與今天的程式進行完整的實際操作與畫面測試:

  1. 正常還書測試:
    操作:

    • 登入系統後點擊導覽列的「我的借閱」進入紀錄頁面。
    • 找到狀態顯示為「借出中」的書籍,點擊旁邊的【歸還】按鈕。

    結果:
    圖片描述
    圖片描述

  2. 逾期提示與天數驗證:
    操作:

    • 手動進入資料庫,將某筆借閱紀錄的 due_date 修改為數天前的日期(例如改為 3 天前)。
    • 重新整理 my_loans.php 頁面。
    • 點擊該筆紀錄旁邊醒目的【逾期歸還】按鈕

    結果:

    • 重新整理後,該筆紀錄的歸還期限與狀態標籤會自動轉為紅色的「已逾期」與「逾期中」。
    • 按下歸還後,頁面上方跳出黃色的訊息提示,驗證時區與逾期天數計算邏輯完全吻合。

今天驗證時區設定,避免了因伺服器 UTC 時差導致的逾期天數判定錯誤,同時實作出了 my_loans.php 前端頁面,完成了圖書借還流程。


上一篇
Day 20 圖書歸還與逾期天數計算
下一篇
Day 22 SQL injection:攻擊自己寫的系統
系列文
從零打造圖書管理系統:WSL2 × MySQL × Claude Code 的整合實作 共 24 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言