前言
该项目是php的基础入门案例。我也是边复习边写的。算个小而比较全的练手案例吧。中间遇到什么问题大家可以评论留言。文档我用的markdown语法写的,排版啥的请大家见谅。还有前置知识,php基础,json,session,类和对象,pdo之类的我默认大家是已经学会了。嗯,这篇文章适合刚刚学完php所有基础做过一两个类似计算器并且没有结构目录案例的朋友。对了,我的构思是这样的。简单的个人中心升级到MVC加发布文章功能加缓存在升级到laravel博客API项目。没错,之前的node接口我不满意,改成php来写了。嘻嘻 ~
PHP 原生 + MySQL 用户个人中心系统
语言:php
数据库: mysql
本项目是一个使用 PHP 原生 + MySQL 开发的用户注册、登录与个人中心管理系统,适合初学者学习用户认证流程、会话管理、数据库操作和基本的安全设计。
🔐 用户注册
- 输入用户名、密码、确认密码
- 检查用户名重复、密码长度(≥6)
- 自动记录注册 IP
- 注册成功后跳转到登录页并自动填充信息
🧾 用户登录
- 输入用户名、密码
- 验证失败计数、记录登录 IP
- 登录成功后保存登录历史
- 登录失败时给出清晰提示
👤 用户中心
- 显示用户名、最后登录时间和 IP
- 支持修改用户名(自动检查重复)
- 显示最近 10 次登录记录(支持排序和分页扩展)
- 修改密码功能(输入旧密码,新密码需确认)
- 密码修改成功后强制退出登录
- 退出按钮返回登录页面
项目信息
personal-center-php-mysql/
├── api/
│ ├── register.php
│ ├── login.php
│ ├── logout.php
│ ├── user.php
│ └── change_password.php
├── func/
│ ├── db.class.php
│ ├── utils.php
│ └── auth.php
├── config/
│ └── config.php
├── public/
│ └── css
│ ├── js
├── templates/
│ ├── register.html
│ ├── login.html
│ └── user_center.html
├── index.php
└── .htaccess
文件说明
api/:包含所有后端 API 脚本。
func/:包含通用函数和类。
config/:包含配置文件。
templates/:包含前端模板文件。
index.php:前端入口文件。
.htaccess:用于配置 URL 重写。
创表语句
CREATE TABLE `users` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '用户唯一标识',
`username` varchar(50) NOT NULL COMMENT '用户名,必须唯一',
`password` varchar(255) NOT NULL COMMENT '用户密码,存储哈希值',
`last_login_ip` varchar(45) DEFAULT NULL COMMENT '用户最后一次登录的IP地址',
`last_login_time` datetime DEFAULT NULL COMMENT '用户最后一次登录的时间',
`registration_ip` varchar(45) DEFAULT NULL COMMENT '用户注册时的IP地址',
`failed_login_attempts` int(11) DEFAULT '0' COMMENT '失败的登录尝试次数',
`created_at` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '用户创建时间',
`updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '用户信息最后更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `username` (`username`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4
CREATE TABLE `login_history` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '登录历史记录唯一标识',
`user_id` int(11) NOT NULL COMMENT '关联的用户ID',
`login_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '登录时间',
`ip_address` varchar(45) DEFAULT NULL COMMENT '登录时的IP地址',
PRIMARY KEY (`id`),
KEY `user_id` (`user_id`),
CONSTRAINT `login_history_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
conf文件
server {
listen 8001;
server_name personal-center-php-mysql;
root "D:/phpstudy_pro/WWW/personal-center-php-mysql";
index index.php;
location / {
index index.php index.html error/index.html;
error_page 400 /error/400.html;
error_page 403 /error/403.html;
error_page 404 /error/404.html;
error_page 500 /error/500.html;
error_page 501 /error/501.html;
error_page 502 /error/502.html;
error_page 503 /error/503.html;
error_page 504 /error/504.html;
error_page 505 /error/505.html;
error_page 506 /error/506.html;
error_page 507 /error/507.html;
error_page 509 /error/509.html;
error_page 510 /error/510.html;
include D:/phpstudy_pro/WWW/personal-center-php-mysql/nginx.htaccess;
autoindex off;
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php(.*)$ {
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
fastcgi_split_path_info ^((?U).+\.php)(/?.+)$;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_param PATH_TRANSLATED $document_root$fastcgi_path_info;
include fastcgi_params;
}
}
必要软件
集成环境小皮面板
编辑器vscode
至于数据库可视化的话,面板里直接下载吧。
github仓库地址:https://github.com/sijidouyyf/personal-center-php-mysql.git
这里github打不开的话你们用下加速器,蹭下免费的用用,用完流量再找个新的就行。加速器
php环境搭建
这里说下我用的是nginx
面板点到网站,在按照图片上写就完事了
在点到设置–文件位置–nginx
他会打开一个文件
然后点开这个文件
D:\phpstudy_pro\Extensions\Nginx1.15.11\conf\vhosts
把信息里的配置粘贴复制进去,这里我解释一下
conf文件解释
我只加了这两句话
index index.php;
try_files $uri $uri/ /index.php?$query_string;
index index.php;
表示:默认首页文件是 index.php
当用户访问目录时(如访问 /),Nginx 会自动尝试打开 index.php 文件。
try_files $uri
u
r
i
/
/
i
n
d
e
x
.
p
h
p
?
uri/ /index.php?
uri//index.php?query_string;
这是伪静态处理的核心逻辑:
Nginx 按顺序尝试三个路径,直到找到一个可用的文件:
$uri → 直接尝试访问请求的原始地址(如 /about.html)
$uri/ → 如果是目录,就尝试加 /(如 /about/)
/index.php? q u e r y s t r i n g → 如果都找不到,就把请求交给 i n d e x . p h p 处理,并保留原始的 G E T 参数( query_string → 如果都找不到,就把请求交给 index.php 处理,并保留原始的 GET 参数( querystring→如果都找不到,就把请求交给index.php处理,并保留原始的GET参数(query_string)
这就实现了 Laravel、ThinkPHP、WordPress 等的路由形式:
例如用户访问 /user/profile,实际 Nginx 会转发为:
index.php?path=/user/profile
这个其实是我们写入口文件要用的知识,以及我们为什么要学linux。毕竟win里我们可以直接粘贴复制。服务器上可是你自己要去配置的。还有跨域配置也是这个文件来写的。当然这个项目不涉及这么多。暂时够用了。下一步~
测试
新建index.php
<?php
echo phpinfo();
?>
开启服务,浏览器输入
http://localhost:8001/
页面输出以下信息成功,否则检查之前的操作,实在不行评论留言或者私信
项目环境搭建
项目目录
按照开始给的目录大家自己新建一下
数据库
按照开始给的sql语句建完库运行一下,名字最好用这个personal_center
,为什么呢,因为我用的是这个。后面代码就可以直接抄了。
入口文件
改写index.php
<?php
// 入口文件
session_start();
// 定义项目根目录
define('ROOT_PATH', dirname(__FILE__));
// 包含配置文件
require_once ROOT_PATH . '/config/config.php';
// 包含数据库类
require_once ROOT_PATH . '/func/db.class.php';
// 包含工具类
require_once ROOT_PATH . '/func/utils.php';
// 包含认证类
require_once ROOT_PATH . '/func/auth.php';
// 获取请求URI
$requestUri = $_SERVER['REQUEST_URI'] ?? '';
// 路由分发
switch ($requestUri) {
case '/register':
require_once ROOT_PATH . '/templates/register.html';
break;
case '/login':
if ($_SERVER['REQUEST_METHOD'] == 'GET') {
require_once ROOT_PATH . '/templates/login.html';
} else if ($_SERVER['REQUEST_METHOD'] == 'POST') {
require_once ROOT_PATH . '/api/login.php';
}
break;
case '/user':
if (isAuthenticated()) {
require_once ROOT_PATH . '/templates/user_center.html';
} else {
echo json_encode(['success' => false, 'message' => '未登录']);
}
break;
case '/change_password':
if (isAuthenticated()) {
require_once ROOT_PATH . '/templates/change_password.html';
} else {
echo json_encode(['success' => false, 'message' => '未登录']);
}
break;
case '/logout':
session_unset();
session_destroy();
header('Location: /login');
exit;
default:
// 默认重定向到登录页
header('Location: /login');
exit;
}
配置文件
新建 config/config.php
说下一般来说mysql默认密码是root。反正你们按照自己的改
<?php
return [
'host' => 'localhost',
'username' => 'root',
'password' => '123456',
'database' => 'personal_center',
'charset' => 'utf8mb4'
];
?>
数据库封装
新建func/db.class.php
文件 。CURD的操作示范写在最下面。这里其实遇到不少事。环境搭建完了我一起说吧
<?php
class Db {
private $host;
private $username;
private $password;
private $database;
private $charset;
private $pdo;
public function __construct() {
$config = require __DIR__ . '/../config/config.php';
$this->host = $config['host'];
$this->username = $config['username'];
$this->password = $config['password'];
$this->database = $config['database'];
$this->charset = $config['charset'];
$this->connect();
}
private function connect() {
try {
$dsn = "mysql:host={$this->host};dbname={$this->database};charset={$this->charset}";
$this->pdo = new PDO($dsn, $this->username, $this->password);
$this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$this->pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
} catch (PDOException $e) {
die("数据库连接失败: " . $e->getMessage());
}
}
public function query($sql, $params = []) {
$stmt = $this->pdo->prepare($sql);
$stmt->execute($params);
return $stmt->fetchAll();
}
public function insert($table, $data) {
$columns = implode(', ', array_keys($data));
$placeholders = ':' . implode(', :', array_keys($data));
$sql = "INSERT INTO {$table} ({$columns}) VALUES ({$placeholders})";
$stmt = $this->pdo->prepare($sql);
$stmt->execute($data);
return $this->pdo->lastInsertId();
}
public function update($table, $data, $conditions) {
$set = [];
$params = []; // 用于存储绑定参数
foreach ($data as $key => $value) {
if ($value === 'NOW()') {
$set[] = "$key = NOW()"; // 直接使用 NOW()
} else {
$set[] = "$key = :$key";
$params[":$key"] = $value; // 仅将非 NOW() 的值添加到参数中
}
}
$set = implode(', ', $set);
$where = [];
foreach ($conditions as $key => $value) {
$where[] = "$key = :$key";
$params[":$key"] = $value; // 将条件参数添加到参数中
}
$where = implode(' AND ', $where);
$sql = "UPDATE {$table} SET {$set} WHERE {$where}";
$stmt = $this->pdo->prepare($sql);
$stmt->execute($params); // 只传递绑定参数
return $stmt->rowCount();
}
public function delete($table, $conditions) {
$where = [];
foreach ($conditions as $key => $value) {
$where[] = "$key = :$key";
}
$where = implode(' AND ', $where);
$sql = "DELETE FROM {$table} WHERE {$where}";
$stmt = $this->pdo->prepare($sql);
$stmt->execute($conditions);
return $stmt->rowCount();
}
public function userExists($username) {
$sql = "SELECT COUNT(*) as count FROM users WHERE username = :username";
$stmt = $this->pdo->prepare($sql);
$stmt->execute(['username' => $username]);
$result = $stmt->fetch(PDO::FETCH_ASSOC);
return $result['count'] > 0;
}
public function beginTransaction() {
return $this->pdo->beginTransaction();
}
public function commit() {
return $this->pdo->commit();
}
public function rollBack() {
return $this->pdo->rollBack();
}
public function __destruct() {
$this->pdo = null;
}
}
// 查询
// $db = new Db();
// $results = $db->query("SELECT * FROM users");
// foreach ($results as $row) {
// print_r($row);
// }
// 插入
// $db = new Db();
// $user_id = $db->insert('users', [
// 'username' => 'testuser',
// 'password' => password_hash('testpassword', PASSWORD_BCRYPT),
// 'registration_ip' => '127.0.0.1'
// ]);
// echo "插入的用户 ID: $user_id";
// 更新
// $db = new Db();
// $updated_rows = $db->update('users', ['password' => password_hash('newpassword', PASSWORD_BCRYPT)], ['id' => 1]);
// echo "更新的行数: $updated_rows";
// 删除
// $db = new Db();
// $deleted_rows = $db->delete('users', ['id' => 1]);
// echo "删除的行数: $deleted_rows";
工具函数和认证函数
新建func/utls.php
.密码用的
<?php
function hashPassword($password) {
return password_hash($password, PASSWORD_DEFAULT);
}
function verifyPassword($password, $hash) {
return password_verify($password, $hash);
}
?>
新建func/auth.php
认证登陆用的
<?php
// 检查用户是否已登录
function isAuthenticated() {
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
return isset($_SESSION['user_id']);
}
// 获取当前用户ID
function getCurrentUserId() {
return $_SESSION['user_id'] ?? null;
}
// 获取当前用户名
function getCurrentUsername() {
return $_SESSION['username'] ?? null;
}
// 登录用户
function loginUser($userId, $username) {
$_SESSION['user_id'] = $userId;
$_SESSION['username'] = $username;
}
// 登出用户
function logoutUser() {
session_unset();
session_destroy();
}
总结
到这里项目配置已经完成。我一个个来说。
涉及知识点:常量定义,包含文件之类的php基础知识不来说了。session,哈希加密,类和对象,PDO。其实该有的技术点差不多都有了,后期无非加功能模块了。
在来说说我遇到的三个坑。
第一个,加载不了db.class.php
require_once __DIR__ . '../func/db.class.php';
首先这段话,大家觉的能加载吗?
没错,不能,会报错
__DIR__ 是绝对路径
../func/db.class.php 是相对路径
这两句话拼接必然报错,所以正确的是
require_once __DIR__ . '/../func/db.class.php';
然后我又自作聪明的把__DIR__ 去掉了。变成了
require_once '../func/db.class.php';
然后有一次莫名其妙的又加载不进来了。我想了想,这玩意依赖目录结构,网上找了一会,发现确实有人不知道
为什么会报错,最后就改成这样了
require_once __DIR__ . '/../func/db.class.php';
第二个,NOW()函数问题
NOW()函数是mysql函数,直接插入会报错。基础类这里我改成接受字符串,用if判断来写了。
这个方式我觉的写的有点问题的。以后在来改进了
第三个,session会话的问题
session原理给大家说下
浏览器 ←→ 服务器
第一次访问:
- 服务端生成 session 文件(保存在服务器)
- 返回给浏览器一个 session_id(通常通过 Cookie)
后续访问:
- 浏览器带着 session_id
- 服务器根据 session_id 找回 session 数据
- 所以可以跨页面共享数据
那问题来了。我的结构也就是说session_start();他在入口文件,对吧。也就是说所有请求都从入口文件走,
所以session是通用的。
然后我遇到坑了。
其实这个问题应该早就想到的。注册那里不会出问题,登陆那里没处理好,seesion肯定会出问题
登陆页面js请求 --- 后端判断设置session 返回成功 --- 跳转到用户中心 用户中心判断是否登陆
这条进程session必须是通的。 然后我这里js请求的路径是这样写的
// 使用fetch提交表单
fetch('../api/login', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert('登录成功');
// 登录成功后的操作
window.location.href = '/user';
} else {
alert('登录失败:' + data.message);
}
})
.catch(error => {
console.error('Error:', error);
alert('登录失败');
});
没错,哥们混了头,直接去找了api/login.php
这个时候session_start();开启在入口文件。我js进程跑到登陆后端,后端设置的
// 登录成功
loginUser($user['id'], $user['username']);
这句话直接无效了。也就是进程跑到用户中心,其实是没有session的。进到用户中心的时候
if (!isAuthenticated()) {
echo json_encode(['success' => false, 'message' => '未登录']);
exit;
}
直接给拦下来了
哎,哥们心塞了,测试了一个小时,才发现这个问题。最后改了入口文件
case '/login':
if ($_SERVER['REQUEST_METHOD'] == 'GET') {
require_once ROOT_PATH . '/templates/login.html';
} else if ($_SERVER['REQUEST_METHOD'] == 'POST') {
require_once ROOT_PATH . '/api/login.php';
}
break;
只能说我这个项目是练手,复习大部分的知识吧。这框架设计的太差了。那里错了改那里。下一个用MVC设计框架
OK,第一章结束。注册,登陆,用户中心,四章结束这个项目。大家加油