资讯动态

PHP图书销售网站实战:表结构、事务与安全防护

发布时间:2026/9/18 11:07:17 来源:尧图企业网站定制
简介一份基于 PHP 的图书销售网站设计与实现文档面向计算机相关专业学生、PHP 初学者以及需要完成课程设计或毕业设计的开发者可作为快速理解电商类 Web 项目的入门参考。文档以单个 docx 文件提供压缩包仅 233KB内容覆盖系统开发背景与目标、PHPMySQL 技术选型、前后台功能模块设计以及功能、性能与安全性测试方案整体结构清晰便于按需查阅。文档围绕商品搜索、用户评论、在线购买等前台模块以及商品管理、用户管理、订单管理等后台模块展开详细说明了各模块的职责划分与实现思路同时涉及 PHP 语言优势、MySQL 数据库应用、电子商务特点、开源社区技术思想等重点知识并给出了从需求到设计再到测试验证的完整路径。已有 185 人学习下载对于需要搭建同类图书销售平台、撰写设计文档或准备答辩的读者而言是一份能够直接借鉴的完整样例也可为后续功能扩展与二次开发奠定基础。1. 基于PHP的图书销售网站一个中型系统的完整样本图书销售网站这个题目表面上看是把图书增删改查搬上网页真正动手后会陷在“边界问题”里购物车数据放哪、库存怎么扣不超卖、订单表怎么设计才能追溯每次交易。这些问题在图书管理系统里不存在在销售网站里却是核心。PHP在这个场景的优点是部署轻、上手快官方文档和开源案例多。实现这个题目的常见顺序是先定表结构再实现商品浏览和购物车然后写带事务和行锁的下单流程接着补上登录鉴权和上传安全最后检查上线前要调整的PHP配置和几组常用的问题排查SQL。无论用PDO原生写还是用ThinkPHP、Laravel表设计和事务逻辑都是相通的。2. 图书销售网站的表结构设计先定数据模型再写PHP代码2.1 模块划分与数据流五张核心表撑起整个销售闭环动手写代码之前先要分清这个项目和图书管理系统的差别。图书管理系统只管管理员对图书的维护不涉及用户下单图书销售网站多了购物车、订单、库存扣减核心模型从“图书”变成“图书订单”。一个最小可运行的销售闭环至少要覆盖五个区域商品浏览、购物车、订单、用户、后台图书维护。商品浏览负责搜索、分类、分页购物车是会话级临时数据订单把购物车内容固化成快照并扣减库存用户模块处理注册登录后台维护图书上下架与库存变更。这些模块之间不是靠页面跳转连接而是靠数据表关系连接。订单明细为什么要存冗余字段因为订单是快照。下单时图书价格是100元一年后图书改成120元历史订单必须仍然显示100元。如果订单明细里只存book_id查询时联表books取价格历史订单的价格就跟着变了对账必然出错。所以order_items表里要有book_title、book_author、price、quantity这些冗余字段它们的语义是“下单那一刻的值”并且和books表解耦。2.2 核心表结构图书、用户、订单三组表的字段设计2.2.1 图书表价格用DECIMAL库存用INT UNSIGNED图书是系统的主数据books表的核心字段和选择理由如下字段类型说明idINT UNSIGNED自增主键titleVARCHAR(255)书名authorVARCHAR(120)作者publisherVARCHAR(120)出版社isbnVARCHAR(20)唯一索引priceDECIMAL(10,2)售价不使用FLOATstockINT UNSIGNED库存不允许负数category_idINT UNSIGNED分类外键cover_imgVARCHAR(255)封面图相对路径statusTINYINT1上架、0下架created_atTIMESTAMP创建时间updated_atTIMESTAMP更新时间价格字段是新手最容易踩坑的地方。FLOAT和DOUBLE是近似存储应用层做累加和比较时会出现精度偏差图书销售的金额计算直接关乎用户付款必须使用DECIMAL(10,2)它按字符串存储精度精确到分。cover_img只存相对路径比如/uploads/2025/book_xx.jpg不存完整URL站点换域名或者接CDN时只需要在模板层拼一次前缀。2.2.2 用户表和订单表密码字段与状态字段用户表的password_hash字段是必选项存放password_hash()函数生成的结果。订单表在设计时要明确一点订单状态用TINYINT整数表示状态迁移由代码控制不允许直接UPDATE为任意值。订单号使用独立的业务号格式如20250612143015012378与自增主键分离。自增id留在内部关联使用对外显示一律用order_no避免用户通过订单号差异推断出平台每日订单量。订单表和订单明细表是一对多关系。orders记录收货人、总金额、状态order_items按行记录每一本书。两个表通过order_id关联并建立外键约束防止程序逻辑出现漏洞时产生没有主订单的明细数据。2.3 建表SQL与索引选择InnoDB、utf8mb4、复合索引-- 图书表 CREATE TABLE books ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, title VARCHAR(255) NOT NULL COMMENT 书名, author VARCHAR(120) NOT NULL, publisher VARCHAR(120), isbn VARCHAR(20) UNIQUE, price DECIMAL(10,2) NOT NULL, stock INT UNSIGNED NOT NULL DEFAULT 0, category_id INT UNSIGNED NOT NULL, cover_img VARCHAR(255), status TINYINT NOT NULL DEFAULT 1 COMMENT 1上架 0下架, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_category_status (category_id, status), KEY idx_title (title) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci; -- 用户表 CREATE TABLE users ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, password_hash VARCHAR(255) NOT NULL, email VARCHAR(120), phone VARCHAR(20), created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci; -- 订单表 CREATE TABLE orders ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, order_no VARCHAR(32) NOT NULL UNIQUE COMMENT 业务订单号, user_id INT UNSIGNED NOT NULL, total_amount DECIMAL(10,2) NOT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待支付 1已支付 2已发货 3已完成 4已取消, receiver_name VARCHAR(50) NOT NULL, receiver_phone VARCHAR(20) NOT NULL, receiver_address VARCHAR(255) NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_user_id (user_id), KEY idx_status_created (status, created_at) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci; -- 订单明细表 CREATE TABLE order_items ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, order_id INT UNSIGNED NOT NULL, book_id INT UNSIGNED NOT NULL, book_title VARCHAR(255) NOT NULL COMMENT 下单时书名快照, book_author VARCHAR(120) DEFAULT COMMENT 下单时作者快照, price DECIMAL(10,2) NOT NULL COMMENT 下单时单价快照, quantity INT UNSIGNED NOT NULL, KEY idx_order_id (order_id), CONSTRAINT fk_order_items_order FOREIGN KEY (order_id) REFERENCES orders(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci;几个决定系统行为的关键选择引擎必须用InnoDB。后面的库存扣减依赖InnoDB的行级锁和事务回滚MyISAM不支持行锁并发下单时整个表会被锁住吞吐量直线下降。utf8mb4不是可选项。MySQL里名为utf8的字符集是utf8mb3最多3字节遇到生僻字或Emoji会插入失败报错utf8mb4才是完整UTF-8编码。加上COLLATEutf8mb4_unicode_ci是为了让排序和比较规则统一。idx_category_status是复合索引覆盖前台最常见的“按分类查上架图书”查询。如果分别在category_id和status上建两个单列索引MySQL一次查询只能选其中一个另一个就要回表性能差一截。orders表的idx_status_created复合索引支撑后台订单列表“按状态筛选并按时间倒序”的查询。order_items的外键建议保留。单体PHP项目中外键不会带来明显性能损失但能在数据库层面拦截孤儿数据比如删除订单时漏删明细这类逻辑Bug。图书和订单数据在几万行以内这套索引足够。几年后订单量超过百万再按order_no独立分表或引入搜索引擎代码层面不需要第一时间推翻。3. 商品浏览、购物车与订单提交会话、事务与行锁的配合3.1 商品列表的分页与筛选条件拼装的规范写法商品列表页最常见的两个筛选条件是分类和关键词。分类字段是精确匹配直接放进WHERE条件关键词是模糊匹配用LIKE实现。动态条件拼接时有一条硬性约定值一律作为PDO绑定参数传递SQL结构固定写死在代码里不要用字符串拼接值。?php // list.php 商品列表分页查询 $page max(1, (int)($_GET[page] ?? 1)); $pageSize 20; $offset ($page - 1) * $pageSize; $conditions [status 1]; $params []; if (isset($_GET[category_id]) $_GET[category_id] ! ) { $conditions[] category_id ?; $params[] (int)$_GET[category_id]; } if (isset($_GET[keyword]) trim($_GET[keyword]) ! ) { $conditions[] title LIKE ?; $params[] % . trim($_GET[keyword]) . %; } $where implode( AND , $conditions); $pdo getPdo(); $stmt $pdo-prepare(SELECT COUNT(*) FROM books WHERE $where); $stmt-execute($params); $total (int)$stmt-fetchColumn(); $stmt $pdo-prepare(SELECT id, title, author, price, cover_img, stock FROM books WHERE $where ORDER BY created_at DESC LIMIT $pageSize OFFSET $offset); $stmt-execute($params); $books $stmt-fetchAll(PDO::FETCH_ASSOC);这段代码有两个容易改坏的细节。第一$where由$conditions数组拼出来这层拼的是固定字段名和操作符而查询值全部在$params里即使keyword带了引号、注释符号也只被当成普通字符串。第二页码必须先转整型再用max(1, ...)限制下限pageSize如果允许外部传入同样要强制转成固定值否则LIMIT和OFFSET会变成注入点。LIMIT和OFFSET也能用参数绑定但不少PHP的MySQL驱动对预处理语句绑定LIMIT支持得不好所以规范做法是像这里一样强转整型再拼进SQL。3.2 购物车的会话级实现数据放Session价格以后端为准购物车数据放哪PHP单机部署的常见做法是放$_SESSION购物车内容以数组形式存在服务器端客户端只保留session_id。不放Cookie的原因有两点Cookie只有4KB左右容量装不下几十件商品更重要的是Cookie内容可以被用户直接修改如果把价格放进Cookie用户改个数字再发回来前后端一对接就会出现按篡改价格下单的结果。?php // cart.php 加入购物车 session_start(); function addToCart(int $bookId, int $qty 1): array { $qty max(1, $qty); $pdo getPdo(); $stmt $pdo-prepare(SELECT id, title, price, stock FROM books WHERE id ? AND status 1); $stmt-execute([$bookId]); $book $stmt-fetch(PDO::FETCH_ASSOC); if (!$book) { return [code 404, msg 图书不存在或已下架]; } if (!isset($_SESSION[cart][$bookId])) { $_SESSION[cart][$bookId] [ title $book[title], price $book[price], qty 0, stock $book[stock] ]; } $newQty $_SESSION[cart][$bookId][qty] $qty; $_SESSION[cart][$bookId][qty] min($newQty, $book[stock]); return [code 200, msg 已加入购物车, cart_count count($_SESSION[cart])]; }加入购物车时用min($newQty, $book[stock])把单本数量限制在库存上限内给用户更快反馈但不依赖它做最终校验。Session里的price字段同样只用于页面展示提交订单时会重新查数据库。还有一点不能偷懒每次addToCart都要查询数据库确认图书存在且上架不要直接信任$_POST[book_id]否则到下单那一刻才暴露问题体验很差。为什么不把购物车直接存RedisRedis适合多实例部署或需要跨设备同步的场景。单机部署时Session已经把数据写到服务器本地省一次网络调用。以后要换Redis把session.save_handler配置成Redis即可业务代码不需要大改。3.3 库存扣减与订单创建事务、行锁和条件更新订单提交是图书销售网站并发压力最集中的代码段。两个用户同时买同一本只剩1库存的书如果先SELECT库存再判断再UPDATE两个请求可能都通过了判断库存最终变成-1。正确做法是把库存扣减和订单写入包在一个事务里用条件更新代替先查后改。?php // checkout.php 提交订单 $pdo-beginTransaction(); try { if (empty($_SESSION[cart])) { throw new RuntimeException(购物车为空); } $items []; $totalAmount 0; // 条件更新只有stock足够时才扣减影响行数为0表示库存不足 $stmtUpdate $pdo-prepare( UPDATE books SET stock stock - ? WHERE id ? AND stock ? ); foreach ($_SESSION[cart] as $bookId $cartItem) { $bookId (int)$bookId; $qty min((int)$cartItem[qty], 99); $stmtUpdate-execute([$qty, $bookId, $qty]); if ($stmtUpdate-rowCount() 0) { throw new RuntimeException(图书 [{$cartItem[title]}] 库存不足); } // 扣减成功后在本事务里读取该图书当前价格 $stmt $pdo-prepare(SELECT title, author, price FROM books WHERE id ?); $stmt-execute([$bookId]); $book $stmt-fetch(PDO::FETCH_ASSOC); $items[] [ book_id $bookId, title $book[title], author $book[author], price $book[price], qty $qty ]; $totalAmount $book[price] * $qty; } $orderNo date(YmdHis) . random_int(1000, 9999); $stmt $pdo-prepare( INSERT INTO orders (order_no, user_id, total_amount, status, receiver_name, receiver_phone, receiver_address) VALUES (?, ?, ?, 0, ?, ?, ?) ); $stmt-execute([ $orderNo, (int)$_SESSION[user_id], $totalAmount, $_POST[receiver_name], $_POST[receiver_phone], $_POST[receiver_address] ]); $orderId (int)$pdo-lastInsertId(); $stmtItem $pdo-prepare( INSERT INTO order_items (order_id, book_id, book_title, book_author, price, quantity) VALUES (?, ?, ?, ?, ?, ?) ); foreach ($items as $item) { $stmtItem-execute([ $orderId, $item[book_id], $item[title], $item[author], $item[price], $item[qty] ]); } unset($_SESSION[cart]); $pdo-commit(); header(Location: order_done.php?order_no . urlencode($orderNo)); exit; } catch (Throwable $e) { $pdo-rollBack(); error_log(sprintf([%s] 订单失败: %s, date(Y-m-d H:i:s), $e-getMessage()), 3, /var/log/php/order_error.log); header(Location: checkout.php?error . urlencode($e-getMessage())); exit; }这段代码的核心在UPDATE books SET stock stock - ? WHERE id ? AND stock ?。这行更新让数据库直接完成“检查-扣减”的原子操作。并发场景下两个事务同时执行它InnoDB会给命中行加写锁前一个事务持有锁后一个等待第一个提交后库存变成0第二个再执行时stock ?不成立影响行数是0代码立刻抛库存不足并回滚不会出现负数库存。这个方案不需要写SELECT ... FOR UPDATE代码更短行为和它等价。3.3.1 事务块内只放写操作查询能少则少事务里面的查询要克制只保留行锁必须覆盖的读操作。上面代码在扣减后重新SELECT了价格这个查询不能移出事务因为需要读取本事务锁定后的最新单价。但商品列表、购物车展示、收货地址列表这些纯查询完全不应该出现在beginTransaction和commit之间。常见坏味道是把整个checkout页面的几十条SQL全塞进事务看起来稳妥实际让行锁持有时间从几毫秒拖到几百毫秒并发能力直线下降排错时也很难定位是哪条SQL拖慢了整体。$qty min((int)$cartItem[qty], 99)限制单本书最多买99本是一个业务保护参数防止异常数据把库存一次扣光。$orderNo用date(YmdHis)加random_int保证高并发下基本不重复且不可猜。random_int生成加密安全随机数比rand更适合订单号这类需要防猜测的值。3.4 下单之后的异步处理什么时候引入php队列刚写完3.3的同步流程后很多人会在支付回调、短信通知上继续加代码逐步把下单请求拖慢。一个常见的架构演进是在订单创建成功后把“发通知、做对账、更新统计”这些非核心操作丢进PHP队列让下单接口尽快返回。用Redis实现队列很简单下单后LPUSH一个JSONworker用BRPOP取出执行。?php // 下单成功后异步发通知 $redisClient-lpush(order:notice, json_encode([ order_no $orderNo, user_id (int)$_SESSION[user_id], ts time() ], JSON_UNESCAPED_UNICODE)); // worker.php 常驻进程 while (true) { $job $redisClient-brpop(order:notice, 5); if ($job) { $data json_decode($job[1], true); sendOrderNotification($data[order_no]); } }这个做法只有在系统真的出现性能瓶颈时才需要引入。刚开始直接在事务提交后调用通知函数更简单也更容易排查。队列的价值是把下单响应的耗时从外部服务的延迟里剥离出去代价是多维护一个worker进程和Redis实例。业务不复杂时不要为了用队列而用队列一旦用了worker的异常重试和日志记录要同步设计好。4. 用户认证与安全加固PHP图书销售网站的防御性编程4.1 注册登录与密码哈希password_hash是唯一正确选项用户密码必须用password_hash()存储。PHP 5.5以上内置这个函数PHP 8默认算法为bcrypt。不要用md5、sha1甚至不要用“md5加盐”这些算法计算速度快攻击者可以用字典暴力破解。password_hash自动生成随机盐并把盐内嵌到哈希字符串里password_verify验证时自动取出盐不需要额外保存。?php // 注册生成密码哈希 $passwordHash password_hash($_POST[password], PASSWORD_DEFAULT); // 登录验证密码 $stmt $pdo-prepare(SELECT id, username, password_hash FROM users WHERE username ?); $stmt-execute([$_POST[username]]); $user $stmt-fetch(PDO::FETCH_ASSOC); if (!$user || !password_verify($_POST[password], $user[password_hash])) { // 统一提示不区分“用户不存在”和“密码错误”防止用户枚举 exit(用户名或密码错误); } // 如果系统后续升级了算法登录成功后重新哈希 if (password_needs_rehash($user[password_hash], PASSWORD_DEFAULT)) { $stmt $pdo-prepare(UPDATE users SET password_hash ? WHERE id ?); $stmt-execute([password_hash($_POST[password], PASSWORD_DEFAULT), $user[id]]); }登录成功后的后续操作有三件必做事把user_id写入$_SESSION、检查用户是否被封禁、跳转时用header(Location)而不是直接输出页面。不要把用户角色、折扣率这类敏感数据存进Session并在业务里直接信任权限判断每次请求都要重新读数据库或缓存。Session固定攻击可以通过配置session.use_strict_mode 1缓解这个设置让PHP只接受服务器端已生成的session_id。4.2 SQL注入防护绑定参数和LIKE过滤SQL注入是PHP Web应用被攻击的高频入口。攻击者通过输入框注入字符串改变SQL结构绕过登录、读取任意数据或删表。防注入的根本原则是所有值都不拼接到SQL语句结构里。?php // 安全写法所有值走绑定参数 $stmt $pdo-prepare(SELECT * FROM books WHERE title ?); $stmt-execute([$_GET[title]]); // 模糊搜索%只作为通配符关键词本身是参数 $stmt $pdo-prepare(SELECT * FROM books WHERE title LIKE ?); $stmt-execute([% . $_GET[keyword] . %]);LIKE场景有个常见误区有人说LIKE需要拼接%号所以LIKE无法防注入。这是把占位符和SQL结构混为一谈了。%是在PHP侧拼进参数值的SQL语句本身仍然是?占位符传入内容只会被当作字符串字面量不会改变SQL语义。后端PHP接口接收参数时尽量做类型约束或白名单校验。$_GET[page]是整型就(int)强转$_POST[email]是邮箱就filter_var($email, FILTER_VALIDATE_EMAIL)验一次。这样可以减少“参数非法导致SQL报错”这类可避免的500错误。4.3 表单安全CSRF令牌与XSS输出过滤订单提交、收货地址修改、后台图书上下架这些操作都必须是“用户主动发起的”。如果页面只校验登录态而不校验来源攻击者可以构造一个自动提交的恶意HTML表单诱导管理员访问后直接改价或删书。CSRF防护的核心是给表单加一次性令牌令牌存在Session里提交时比对。?php session_start(); // 生成令牌 if (empty($_SESSION[csrf_token])) { $_SESSION[csrf_token] bin2hex(random_bytes(32)); } // 表单里输出 echo input typehidden namecsrf_token value . htmlspecialchars($_SESSION[csrf_token], ENT_QUOTES, UTF-8) . ; // 提交时校验 if (!hash_equals($_SESSION[csrf_token], $_POST[csrf_token] ?? )) { http_response_code(400); exit(CSRF校验失败); }hash_equals用恒定时间比较不会因字符串内容不同产生明显时间差避免时序侧信道。XSS防护集中在输出端所有从数据库或用户输入回显到HTML的内容都过一遍htmlspecialchars($value, ENT_QUOTES, UTF-8)。书名、作者、商品描述这些字段管理员可以填写如果不转义直接输出后台就成了存储型XSS的发射点。Twig、Blade这类模板引擎默认转义用原生PHP写模板时最容易漏掉这一步。前端调PHP接口还会遇到另一个跨域场景。搜索php跨域jsonp的人很多但JSONP本身有安全缺陷它靠动态创建script标签实现跨域响应内容会被当作JavaScript执行回调函数名拼接不当会引入XSS。现代方案是CORSPHP接口里设置header(Access-Control-Allow-Origin: ...)没有JSONP的回调注入风险浏览器兼容性也足够。4.4 文件上传、反序列化与错误处理4.4.1 文件上传类型、后缀和目录三级防御后台需要上传封面图文件上传是最容易被攻击者利用的入口。一个典型攻击路径是攻击者伪造Content-Type上传一个包含PHP代码的图片文件如果服务器把它当PHP执行管理员后台就沦陷了。防御分三层。?php // 第一层读取文件真实MIME类型不信任$_FILES[cover][type] $allowedMime [image/jpeg, image/png, image/webp]; $finfo finfo_open(FILEINFO_MIME_TYPE); $mime finfo_file($finfo, $_FILES[cover][tmp_name]); finfo_close($finfo); if (!in_array($mime, $allowedMime, true)) { exit(仅支持JPG/PNG/WebP图片); } // 第二层用随机文件名替换用户原始文件名 $ext [image/jpeg jpg, image/png png, image/webp webp][$mime]; $newName date(Ymd) . _ . bin2hex(random_bytes(8)) . . . $ext; move_uploaded_file($_FILES[cover][tmp_name], __DIR__ . /uploads/ . $newName);第三层是把上传目录放在Web根目录之外或在Nginx配置里禁止uploads目录解析PHP。上传目录如果允许执行PHP前面所有类型校验都可能被解析漏洞绕过。不要使用用户上传的原始文件名这一点往往比MIME校验更容易被忽略。4.4.2 反序列化风险与错误处理闭环PHP反序列化漏洞和图书销售站点的关联点在会话与回调数据上。如果业务代码用unserialize()处理外部输入攻击者可以通过构造序列化数据注入对象在反序列化时触发魔术方法形成代码执行。在购物车、支付回调、用户导入这些场景数据统一用json_encode/json_decode不使用serialize/unserialize。JSON只保存普通数组和标量没有对象构造与魔术方法触发面。PHP错误处理的最后一道闸门是日志。线上环境display_errors必须关闭否则SQL报错会把表名、连接信息直接暴露在响应里。error_log要配置到独立文件并确认目录可写。上面checkout代码中写下过一行error_log(sprintf([%s] 订单失败: %s, date(Y-m-d H:i:s), $e-getMessage()), 3, /var/log/php/order_error.log);这比exit($e-getMessage())强在两点第一用户看到的是统一错误提示不泄露内部实现第二完整堆栈进了日志排错时有据可查。5. 基于PHP图书销售网站的上线检查与故障排查技巧5.1 上线前必须检查的PHP配置项display_errors Off、error_log路径可写、date.timezone设置正确这三项是上线前必查的。display_errors开着遇到SQL异常时整个错误页会带出数据库表和字段信息date.timezone不设置订单创建时间和日志时间会差8小时排查问题非常痛苦。用Docker打包PHP镜像时这些配置也要在镜像内同步修改不要只改宿主机上的php.ini。; php.ini 关键项 display_errors Off log_errors On error_log /var/log/php/php_errors.log date.timezone Asia/Shanghai session.use_strict_mode 1session.use_strict_mode 1让PHP只接受服务器端已有记录对应的session_id能挡住一部分会话固定攻击。有个很少人注意的细节修改php.ini后用php-fpm restart而不是php-fpm reloadsession相关配置在reload时可能没有重新加载。5.2 三组可以直接执行的监控SQL业务上线后最容易出现异常的是库存、订单和慢SQL。运营第一周可以每天执行下面三组SQL。-- 1. 检查库存是否有负数或超常值 SELECT id, title, stock FROM books WHERE stock 0 OR stock 10000; -- 2. 找出超过24小时未支付的孤儿订单 SELECT order_no, user_id, total_amount, created_at FROM orders WHERE status 0 AND created_at NOW() - INTERVAL 1 DAY ORDER BY created_at DESC LIMIT 20; -- 3. 确认慢查询开关和阈值 SHOW VARIABLES LIKE long_query_time;第一组SQL只要跑出负数说明事务代码被改坏了或者有人绕过程序直接改库。第二组找出待支付超24小时的订单配合定时任务把它们改成取消状态并回滚库存。第三组要关注的是long_query_time默认10秒过滤太宽建议调到2秒然后定期看slow_query_log里有没有在订单表上做全表扫描的SQL。5.3 一个能长期复用的上线验证方法我在负责的项目里把上线从人工点页面改成了一套五步验证。先用curl模拟未登录用户访问商品列表确认返回200再带Cookie访问购物车接口加入一件商品接着提交订单并检查响应是否带order_no再用数据库查询确认orders和order_items各多了一行最后看order_error.log有没有新增错误。五步跑通再开放线上流量。这个验证方法的价值不是自动化而是把业务主链路拆成五个显式检查点每个检查点失败都能立刻定位问题在哪个环节。代码改动后在本地跑一遍同样的五步基本能拦住大部分回归型Bug。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价