Skip to content

13.5 实操:用户头像上传

本章节将通过一个完整的实操示例,实现用户头像上传功能。

项目结构

avatar-upload/
├── db.php              # 数据库连接文件
├── index.php           # 登录页面
├── profile.php         # 个人资料页面
├── upload_avatar.php   # 头像上传处理
├── logout.php          # 退出登录
├── default-avatar.png  # 默认头像
└── uploads/            # 上传目录
    └── avatars/        # 头像存储目录

1. 数据库配置

创建 db.php 文件,用于数据库连接:

php
<?php
// db.php
function getDbConnection() {
    $servername = "localhost";
    $username = "root";
    $password = "";
    $dbname = "php_tutorial";
    
    // 创建连接
    $conn = new mysqli($servername, $username, $password, $dbname);
    
    // 检查连接
    if ($conn->connect_error) {
        die("连接失败: " . $conn->connect_error);
    }
    
    // 设置字符集
    $conn->set_charset("utf8mb4");
    
    return $conn;
}
?>

2. 数据库表结构

创建 users 表:

sql
CREATE TABLE users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(50) NOT NULL UNIQUE,
    email VARCHAR(100) NOT NULL UNIQUE,
    password VARCHAR(255) NOT NULL,
    avatar VARCHAR(255) DEFAULT 'default-avatar.png',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- 插入测试用户
INSERT INTO users (username, email, password) VALUES
('testuser', 'test@example.com', 'password123');

3. 登录页面

创建 index.php 文件:

php
<?php
// index.php
session_start();

// 如果已登录,跳转到个人资料页面
if (isset($_SESSION['user_id'])) {
    header('Location: profile.php');
    exit;
}

$error = '';

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    require_once 'db.php';
    $conn = getDbConnection();
    
    $username = $_POST['username'];
    $password = $_POST['password'];
    
    $stmt = $conn->prepare("SELECT id, username, avatar FROM users WHERE username = ? AND password = ?");
    $stmt->bind_param("ss", $username, $password);
    $stmt->execute();
    $result = $stmt->get_result();
    
    if ($result->num_rows > 0) {
        $user = $result->fetch_assoc();
        $_SESSION['user_id'] = $user['id'];
        $_SESSION['username'] = $user['username'];
        $_SESSION['avatar'] = $user['avatar'];
        header('Location: profile.php');
        exit;
    } else {
        $error = '用户名或密码错误';
    }
    
    $stmt->close();
    $conn->close();
}
?>

<!DOCTYPE html>
<html>
<head>
    <title>登录</title>
    <style>
        body { font-family: Arial, sans-serif; max-width: 400px; margin: 0 auto; padding: 20px; }
        h1 { text-align: center; color: #333; }
        .form-group { margin: 15px 0; }
        label { display: block; margin-bottom: 5px; font-weight: bold; }
        input[type="text"], input[type="password"] {
            padding: 10px; border: 1px solid #ddd; border-radius: 4px; width: 100%; box-sizing: border-box;
        }
        input[type="submit"] {
            padding: 10px 20px; background-color: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer; width: 100%;
        }
        .error { padding: 10px; margin: 10px 0; border-radius: 4px; background-color: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
    </style>
</head>
<body>
    <h1>用户登录</h1>
    
    <?php if (!empty($error)): ?>
        <div class="error"><?php echo $error; ?></div>
    <?php endif; ?>
    
    <form action="" method="post">
        <div class="form-group">
            <label>用户名: <input type="text" name="username" required></label>
        </div>
        <div class="form-group">
            <label>密码: <input type="password" name="password" required></label>
        </div>
        <input type="submit" value="登录">
    </form>
    
    <p style="text-align: center; margin-top: 20px;">测试账号: testuser / password123</p>
</body>
</html>

4. 个人资料页面

创建 profile.php 文件:

php
<?php
// profile.php
session_start();

// 检查是否登录
if (!isset($_SESSION['user_id'])) {
    header('Location: index.php');
    exit;
}

require_once 'db.php';
$conn = getDbConnection();

$userId = $_SESSION['user_id'];

// 获取用户信息
$stmt = $conn->prepare("SELECT username, email, avatar FROM users WHERE id = ?");
$stmt->bind_param("i", $userId);
$stmt->execute();
$result = $stmt->get_result();

if ($result->num_rows > 0) {
    $user = $result->fetch_assoc();
    $avatarUrl = 'uploads/avatars/' . ($user['avatar'] ?: 'default-avatar.png');
    if (!file_exists($avatarUrl)) {
        $avatarUrl = 'default-avatar.png';
    }
} else {
    header('Location: index.php');
    exit;
}

$stmt->close();
$conn->close();
?>

<!DOCTYPE html>
<html>
<head>
    <title>个人资料</title>
    <style>
        body { font-family: Arial, sans-serif; max-width: 500px; margin: 0 auto; padding: 20px; }
        h1 { text-align: center; color: #333; }
        .profile { text-align: center; margin: 20px 0; }
        .avatar { width: 150px; height: 150px; border-radius: 50%; object-fit: cover; }
        .user-info { margin: 20px 0; }
        .form-group { margin: 15px 0; }
        label { display: block; margin-bottom: 5px; font-weight: bold; }
        input[type="file"] { padding: 10px; border: 1px solid #ddd; border-radius: 4px; width: 100%; box-sizing: border-box; }
        input[type="submit"] { padding: 10px 20px; background-color: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer; }
        .message { padding: 10px; margin: 10px 0; border-radius: 4px; }
        .success { background-color: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
        .error { background-color: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
        .logout { text-align: center; margin-top: 20px; }
        .logout a { color: #dc3545; text-decoration: none; }
        .logout a:hover { text-decoration: underline; }
    </style>
</head>
<body>
    <h1>个人资料</h1>
    
    <div class="profile">
        <img src="<?php echo $avatarUrl; ?>" alt="头像" class="avatar">
        <h2><?php echo $user['username']; ?></h2>
        <p><?php echo $user['email']; ?></p>
    </div>
    
    <?php if (isset($_GET['success'])): ?>
        <div class="message success">
            <?php echo $_GET['success']; ?>
        </div>
    <?php endif; ?>
    
    <form action="upload_avatar.php" method="post" enctype="multipart/form-data">
        <div class="form-group">
            <label>更换头像: <input type="file" name="avatar" accept="image/*" required></label>
        </div>
        <input type="submit" value="上传头像" name="submit">
    </form>
    
    <div class="logout">
        <a href="logout.php">退出登录</a>
    </div>
</body>
</html>

5. 头像上传处理

创建 upload_avatar.php 文件:

php
<?php
// upload_avatar.php
session_start();

// 检查是否登录
if (!isset($_SESSION['user_id'])) {
    header('Location: index.php');
    exit;
}

require_once 'db.php';
$conn = getDbConnection();

$targetDir = "uploads/avatars/";
$maxFileSize = 2 * 1024 * 1024; // 2MB
$allowedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
$errors = [];

// 确保上传目录存在
if (!is_dir($targetDir)) {
    mkdir($targetDir, 0755, true);
}

$userId = $_SESSION['user_id'];

if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['avatar'])) {
    $file = $_FILES['avatar'];
    
    // 检查是否有错误
    if ($file['error'] !== 0) {
        $errors[] = '上传失败,错误代码: ' . $file['error'];
    } else {
        // 检查文件大小
        if ($file['size'] > $maxFileSize) {
            $errors[] = "文件太大,最大允许 2MB";
        }
        
        // 检查文件扩展名
        $fileExtension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
        if (!in_array($fileExtension, $allowedExtensions)) {
            $errors[] = "只允许上传图片文件(JPG、PNG、GIF、WebP)";
        }
        
        // 检查是否为真实图片
        if (getimagesize($file['tmp_name']) === false) {
            $errors[] = "请上传真实的图片文件";
        }
        
        // 处理上传
        if (empty($errors)) {
            // 生成新文件名
            $newFileName = uniqid() . '.' . $fileExtension;
            $targetFile = $targetDir . $newFileName;
            
            if (move_uploaded_file($file['tmp_name'], $targetFile)) {
                // 更新数据库
                $stmt = $conn->prepare("UPDATE users SET avatar = ? WHERE id = ?");
                $stmt->bind_param("si", $newFileName, $userId);
                
                if ($stmt->execute()) {
                    // 更新会话中的头像信息
                    $_SESSION['avatar'] = $newFileName;
                    header('Location: profile.php?success=头像上传成功');
                    exit;
                } else {
                    $errors[] = "更新数据库失败";
                }
                
                $stmt->close();
            } else {
                $errors[] = "上传失败,请重试";
            }
        }
    }
}

$conn->close();

// 如果有错误,跳回个人资料页面
if (!empty($errors)) {
    $errorMsg = urlencode(implode('; ', $errors));
    header('Location: profile.php?error=' . $errorMsg);
    exit;
}
?>

6. 退出登录

创建 logout.php 文件:

php
<?php
// logout.php
session_start();

// 清除会话变量
session_unset();

// 销毁会话
session_destroy();

// 跳转到登录页面
header('Location: index.php');
exit;
?>

7. 运行项目

  1. 确保数据库已创建并插入测试用户
  2. 启动本地服务器
  3. 访问 http://localhost/avatar-upload/index.php
  4. 使用测试账号登录:
    • 用户名: testuser
    • 密码: password123
  5. 上传头像并查看效果

8. 功能扩展

1. 添加图片预览功能

修改 profile.php 文件,添加图片预览功能:

html
<!-- 在表单中添加预览区域 -->
<div class="form-group">
    <label>更换头像: <input type="file" name="avatar" accept="image/*" required onchange="previewImage(this);"></label>
    <div id="preview-container" style="margin-top: 10px; display: none;">
        <img id="preview" src="" alt="预览" style="width: 100px; height: 100px; border-radius: 50%; object-fit: cover;">
    </div>
</div>

<script>
function previewImage(input) {
    if (input.files && input.files[0]) {
        const previewContainer = document.getElementById('preview-container');
        const preview = document.getElementById('preview');
        
        previewContainer.style.display = 'block';
        
        const reader = new FileReader();
        reader.onload = function(e) {
            preview.src = e.target.result;
        };
        reader.readAsDataURL(input.files[0]);
    }
}
</script>

2. 添加头像裁剪功能

使用第三方库如 Cropper.js 添加头像裁剪功能:

html
<!-- 在 head 中添加 Cropper.js 的 CSS -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.13/cropper.min.css">

<!-- 在 body 底部添加 Cropper.js 的 JS -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.13/cropper.min.js"></script>

<!-- 修改表单 -->
<form action="upload_avatar.php" method="post" enctype="multipart/form-data">
    <div class="form-group">
        <label>更换头像: <input type="file" name="avatar" accept="image/*" required onchange="openCropper(this);"></label>
    </div>
    
    <!-- 裁剪模态框 -->
    <div id="crop-modal" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.8); z-index: 1000;">
        <div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background-color: white; padding: 20px; border-radius: 8px;">
            <h3>裁剪头像</h3>
            <div style="width: 300px; height: 300px; overflow: hidden;">
                <img id="crop-image" src="" alt="裁剪" style="max-width: 100%;">
            </div>
            <div style="margin-top: 20px; text-align: center;">
                <button type="button" onclick="cancelCrop();" style="padding: 10px 20px; background-color: #6c757d; color: white; border: none; border-radius: 4px; cursor: pointer; margin-right: 10px;">取消</button>
                <button type="button" onclick="cropImage();" style="padding: 10px 20px; background-color: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer;">确认</button>
            </div>
        </div>
    </div>
    
    <input type="hidden" name="croppedImage" id="croppedImage">
    <input type="submit" value="上传头像" name="submit" style="display: none;">
</form>

<script>
let cropper;

function openCropper(input) {
    if (input.files && input.files[0]) {
        const cropModal = document.getElementById('crop-modal');
        const cropImage = document.getElementById('crop-image');
        
        cropModal.style.display = 'block';
        
        const reader = new FileReader();
        reader.onload = function(e) {
            cropImage.src = e.target.result;
            
            // 初始化 Cropper.js
            cropper = new Cropper(cropImage, {
                aspectRatio: 1,
                viewMode: 1,
                dragMode: 'move',
                autoCropArea: 0.8,
                cropBoxMovable: true,
                cropBoxResizable: true
            });
        };
        reader.readAsDataURL(input.files[0]);
    }
}

function cancelCrop() {
    const cropModal = document.getElementById('crop-modal');
    cropModal.style.display = 'none';
    
    // 销毁 Cropper 实例
    if (cropper) {
        cropper.destroy();
        cropper = null;
    }
}

function cropImage() {
    if (cropper) {
        // 获取裁剪后的图片数据
        const canvas = cropper.getCroppedCanvas({
            width: 300,
            height: 300
        });
        
        // 将裁剪后的图片转换为 base64 格式
        const croppedImage = canvas.toDataURL('image/jpeg');
        
        // 设置隐藏字段的值
        document.getElementById('croppedImage').value = croppedImage;
        
        // 关闭模态框
        cancelCrop();
        
        // 提交表单
        document.querySelector('form').submit();
    }
}
</script>

然后修改 upload_avatar.php 文件,处理裁剪后的图片:

php
// 处理裁剪后的图片
if (isset($_POST['croppedImage'])) {
    $croppedImage = $_POST['croppedImage'];
    
    // 移除 base64 前缀
    $croppedImage = str_replace('data:image/jpeg;base64,', '', $croppedImage);
    
    // 解码 base64 数据
    $croppedImage = base64_decode($croppedImage);
    
    // 生成新文件名
    $newFileName = uniqid() . '.jpg';
    $targetFile = $targetDir . $newFileName;
    
    // 保存裁剪后的图片
    if (file_put_contents($targetFile, $croppedImage)) {
        // 更新数据库
        $stmt = $conn->prepare("UPDATE users SET avatar = ? WHERE id = ?");
        $stmt->bind_param("si", $newFileName, $userId);
        
        if ($stmt->execute()) {
            // 更新会话中的头像信息
            $_SESSION['avatar'] = $newFileName;
            header('Location: profile.php?success=头像上传成功');
            exit;
        } else {
            $errors[] = "更新数据库失败";
        }
        
        $stmt->close();
    } else {
        $errors[] = "上传失败,请重试";
    }
} else {
    // 原有的文件上传处理逻辑
    // ...
}

3. 添加图片压缩功能

使用 GD 库对上传的图片进行压缩:

php
// 压缩图片
function compressImage($sourcePath, $targetPath, $quality = 80) {
    list($sourceWidth, $sourceHeight) = getimagesize($sourcePath);
    
    $sourceImage = imagecreatefromjpeg($sourcePath);
    $targetImage = imagecreatetruecolor($sourceWidth, $sourceHeight);
    
    imagecopyresampled($targetImage, $sourceImage, 0, 0, 0, 0, $sourceWidth, $sourceHeight, $sourceWidth, $sourceHeight);
    
    $result = imagejpeg($targetImage, $targetPath, $quality);
    
    imagedestroy($sourceImage);
    imagedestroy($targetImage);
    
    return $result;
}

// 使用示例
if (move_uploaded_file($file['tmp_name'], $targetFile)) {
    // 压缩图片
    compressImage($targetFile, $targetFile, 80);
    
    // 更新数据库
    // ...
}

9. 注意事项

  1. 安全性

    • 验证上传文件的类型和大小
    • 重命名文件,避免文件名冲突
    • 限制上传目录的权限
    • 不要将上传的文件直接存储在 web 根目录外
  2. 性能

    • 对上传的图片进行压缩
    • 生成缩略图,提高加载速度
  3. 用户体验

    • 提供图片预览功能
    • 支持图片裁剪
    • 显示上传进度
    • 提供清晰的错误提示
  4. 兼容性

    • 支持不同浏览器的文件上传
    • 考虑移动设备的上传体验
    • 提供默认头像

练习

  1. 实现完整的用户头像上传系统
  2. 添加图片预览和裁剪功能
  3. 实现图片压缩功能
  4. 优化用户界面和体验
  5. 添加多尺寸头像生成(如小、中、大尺寸)

© 2026 编程马·菜鸟教程 版权所有