CREATE TABLE `ip_vote_log` (
`ip` VARBINARY(16) NOT NULL COMMENT 'inet_pton() 二進制 IP',
`vote_date` DATE NOT NULL COMMENT '投票日期',
`item_id` INT UNSIGNED NOT NULL COMMENT '投票項目 ID (可選)',
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`ip`, `vote_date`, `item_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='IP 每日投票記錄';
<?php
/**
* 檢查並記錄投票
* @param int $itemId 投票項目 ID (單項目可固定為 1)
* @return array ['success'=>bool, 'message'=>string]
*/
function processVote(int $itemId = 1): array {
$mysqli = new mysqli('localhost', 'user', 'pass', 'vote_db');
$mysqli->set_charset('utf8mb4');
// 獲取真實 IP (需配合 CDN/代理環境)
$ip = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR'];
$ip = trim(explode(',', $ip)[0]); // 取第一個
$ipBin = inet_pton($ip);
$today = date('Y-m-d');
// 1. 嘗試插入投票記錄 (原子操作)
$sql = "
INSERT IGNORE INTO ip_vote_log (ip, vote_date, item_id)
VALUES (?, ?, ?)
";
$stmt = $mysqli->prepare($sql);
$stmt->bind_param('bsi', $ipBin, $today, $itemId);
$stmt->execute();
if ($mysqli->affected_rows > 0) {
// 插入成功 = 首次投票
return ['success' => true, 'message' => '投票成功!'];
} else {
// 插入失敗 = 重複投票 (主鍵衝突)
return ['success' => false, 'message' => '您今天已經投過票了,請明天再來。'];
}
}
// 使用示例
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$result = processVote((int)($_POST['item_id'] ?? 1));
header('Content-Type: application/json');
echo json_encode($result);
exit;
}
?>
<!DOCTYPE html>
<html>
<body>
<h2>投票頁面</h2>
<form method="post">
<input type="hidden" name="item_id" value="1">
<button type="submit">投票支持</button>
</form>
<div id="msg"></div>
<script>
document.querySelector('form').addEventListener('submit', async (e) => {
e.preventDefault();
const fd = new FormData(e.target);
const res = await fetch('', {method:'POST', body:fd}).then(r=>r.json());
document.getElementById('msg').innerHTML =
res.success ? '<span style="color:green">'+res.message+'</span>'
: '<span style="color:red">'+res.message+'</span>';
});
</script>
</body>
</html>
| 技術點 | 說明 |
|---|---|
| INSERT IGNORE | 主鍵衝突時不報錯、不回滾、affected_rows=0,完美適配「已存在則忽略」語義 |
| 聯合主鍵 (ip, vote_date, item_id) | 保證同 IP、同天、同項目唯一;支持多項目獨立投票 |
| VARBINARY(16) + inet_pton() | 高效存儲 IPv4/IPv6,支持索引範圍查詢,比 VARCHAR(45) 節省空間 |
| 日期分區 | 隱含在 vote_date 字段,配合定時任務 DROP PARTITION 或 DELETE WHERE vote_date < ? 清理 |
user_id 唯一約束,徹底解決 IP 共用問題 (公司/學校/家庭共用出口 IP)SETNX ip_vote:{ip}:{date}:{item} 快速攔截,減輕 MySQL 壓力<?php
// cleanup_vote_log.php - 每月 1 號執行
$mysqli = new mysqli('localhost', 'user', 'pass', 'vote_db');
$keepMonths = 6;
$cutoff = date('Y-m-d', strtotime("-$keepMonths months"));
$mysqli->query("DELETE FROM ip_vote_log WHERE vote_date < '$cutoff'");
echo "清理 $cutoff 前數據,影響行數: " . $mysqli->affected_rows;
?>
若業務允許「修改投票」,將 INSERT IGNORE 改為 INSERT ... ON DUPLICATE KEY UPDATE 並記錄 updated_at,但需權衡「一次投票」原則。