1. 项目概述:为什么在VPS上手敲CodeIgniter才是学透MVC的正道
CodeIgniter,这个被很多老PHP开发者称为“轻量级瑞士军刀”的框架,至今仍在中小项目、内部工具和教学场景里稳坐一席之地。它不靠花哨的命令行生成器,不堆砌抽象层,而是用最直白的PHP语法把MVC的骨架一根一根搭出来——路由怎么映射到控制器,控制器怎么调用模型取数据,视图怎么安全地渲染变量,全摊开在你眼皮底下。而VPS,就是这张“摊开的纸”最真实的载体。不是本地XAMPP那种一切都被封装好的黑盒,也不是Docker容器里隔了一层抽象的镜像,一台干净的VPS,从SSH登录那一刻起,你面对的就是真实世界的Linux环境:
/var/www/html
目录权限怎么设、Apache的
.htaccess
重写规则为什么失效、MySQL用户权限颗粒度怎么控制、PHP扩展缺了
mbstring
会导致中文乱码……这些细节,恰恰是理解CodeIgniter底层逻辑绕不开的台阶。
我带过不少刚转PHP的前端和运维朋友,他们常犯一个错:直接在本地装个WAMP,跑通
http://localhost/welcome
就以为学会了。结果一上生产环境,URL重写404、数据库连接超时、上传文件失败,全懵了。真正把CodeIgniter吃透,必须亲手在VPS上走一遍完整链路:从系统初始化、Web服务器配置、PHP环境调优,到框架安装、路由调试、模型封装、视图安全输出。这过程里,你会自然理解为什么CodeIgniter的
index.php
是唯一入口、为什么
application/config/config.php
里
base_url()
要手动配、为什么
routes.php
里
$route['default_controller'] = 'welcome';
这行代码背后牵扯着整个URI解析流程。VPS不是炫技的玩具,它是你和PHP世界之间最诚实的接口——没有魔法,只有配置、权限、路径和日志。这篇文章,就是带你用一台最基础的VPS(哪怕是最便宜的甲骨文免费VPS),从零开始,把CodeIgniter的筋骨摸清楚。适合所有想甩掉“只会复制粘贴Demo”的PHP初学者,也适合需要快速搭建轻量后台的运维或全栈工程师。你不需要懂Docker,不需要会写Shell脚本,只要能连上SSH,就能跟着一步步做出来。
2. 环境准备与架构设计:VPS选型、系统初始化与CodeIgniter部署策略
2.1 VPS选型逻辑:为什么“甲骨文VPS”和“腾讯云轻量应用服务器”是新手最优解
选VPS不是比谁CPU核数多、内存大,而是看三个硬指标:
SSH直连稳定性、root权限开放度、系统镜像纯净度
。很多新手一上来就选高配ECS,结果发现默认禁用root、SSH密钥登录强制开启、预装一堆监控Agent,反而把最基础的
apt update && apt install php
卡在权限验证上。甲骨文云(Oracle Cloud)的免费VPS,虽然限制2核1G,但胜在三点:第一,提供完整的Ubuntu 22.04 LTS官方镜像,无任何预装软件;第二,root密码可自定义,SSH直连无额外认证关卡;第三,网络延迟对国内用户相对友好,
apt
源更新速度实测比某些小众VPS快3倍以上。腾讯云轻量应用服务器同理,选择“LAMP应用镜像”后,它预装的是Apache+PHP+MySQL组合,但关键在于——它把所有服务的配置文件路径、日志位置、启动命令都标准化了,你不用猜
php.ini
在
/etc/php/8.1/apache2/
还是
/etc/php/8.1/cli/
,直接
ls /etc/php/*/apache2/
就能看到。而那些标榜“一键部署PHP环境”的面板类VPS(如宝塔),看似省事,实则埋雷:它的Nginx配置被深度魔改,
.htaccess
重写规则完全失效,CodeIgniter依赖的
mod_rewrite
模块可能被禁用,你后期排查路由问题时,会发现90%的精力耗在跟面板斗智斗勇上。
提示:本文全程以Ubuntu 22.04 + Apache2 + PHP 8.1 + MySQL 8.0为基准环境。如果你用CentOS,请自动将
apt替换为dnf,/var/www/html路径保持不变,但PHP配置文件路径会变成/etc/php.d/目录下多个ini文件。切记不要强行套用命令,先执行ls /etc/php*确认结构。
2.2 系统初始化四步法:从裸机到可部署状态
VPS买好后,拿到IP、用户名、密码,别急着装CodeIgniter。先做四件事,这是后续所有操作稳定的基石:
第一步:基础安全加固
用SSH登录后,立即执行:
sudo apt update && sudo apt upgrade -y
sudo apt install ufw -y
sudo ufw allow OpenSSH
sudo ufw enable
这三行命令干了三件事:更新系统补丁(堵住已知漏洞)、安装防火墙、只放行SSH端口(22)。很多新手跳过这步,结果VPS上线两小时就被扫出弱密码,挂马挖矿。UFW是Ubuntu自带的简易防火墙,比iptables命令友好十倍,
ufw status
就能看到当前规则。
第二步:创建非root部署用户
永远不要用root用户跑Web服务。执行:
sudo adduser ciuser
sudo usermod -aG sudo ciuser
sudo su - ciuser
创建
ciuser
用户并赋予sudo权限。后续所有CodeIgniter文件都归此用户所有,Apache进程以
www-data
用户运行,通过Linux文件权限(
chown -R ciuser:www-data /var/www/ci
)实现安全隔离。这是PHP项目防文件遍历攻击的第一道门。
第三步:安装LAMP核心组件
sudo apt install apache2 mysql-server php libapache2-mod-php php-mysql php-curl php-gd php-mbstring php-xml php-xmlrpc php-soap php-intl php-zip -y
注意这个命令里的扩展列表:
php-mbstring
处理中文字符,
php-xml
是CodeIgniter加载配置必需的,
php-zip
用于后续可能的自动更新。漏掉任何一个,CodeIgniter启动时都会报
Class 'CI_Exceptions' not found
这类诡异错误。安装完重启Apache:
sudo systemctl restart apache2
。
第四步:验证PHP环境是否就绪
在
/var/www/html/
下新建
info.php
:
<?php phpinfo(); ?>
浏览器访问
http://你的VPS_IP/info.php
,重点检查三处:
-
Loaded Configuration File
:确认路径是
/etc/php/8.1/apache2/php.ini(不是cli版本) -
extension_dir
:值应为
/usr/lib/php/20210902(PHP 8.1对应值) -
Loaded Extensions
:滚动查找
mysqli,mbstring,xml是否在列表中
如果
mbstring
没出现,说明扩展没启用,执行
sudo phpenmod mbstring
再重启Apache。这一步卡住的人最多,因为错误提示极其隐晦——CodeIgniter只是静默失败,页面空白,日志里只有一行
PHP Fatal error: Uncaught Error: Call to undefined function mb_strlen()
。
2.3 CodeIgniter部署策略:为什么放弃Composer而选择手动下载压缩包
CodeIgniter官网提供两种安装方式:Composer全局安装和ZIP压缩包手动解压。新手务必选后者。原因有三:
第一,Composer在VPS上首次安装需翻墙下载
https://getcomposer.org/installer
,而国内网络环境下大概率超时失败,报错
cURL error 28: Operation timed out after 300000 milliseconds
。手动下载ZIP包(约2MB)用
wget
命令极快:
wget https://codeload.github.com/bcit-ci/CodeIgniter/zip/refs/tags/3.1.13
。
第二,Composer安装会把框架文件散落在
vendor/
目录,而CodeIgniter的核心逻辑(
system/
和
application/
)必须严格位于Web根目录下。手动解压后,你能清晰看到
/var/www/ci/system/
和
/var/www/ci/application/
的物理路径,这对理解
index.php
里
$system_path
和
$application_folder
变量的指向至关重要。
第三,手动部署便于做最小化裁剪。比如你确定不用邮件功能,可以直接删掉
system/libraries/Email.php
,而Composer安装后删文件会导致
autoload
机制报错。
部署路径定为
/var/www/ci/
,这是行业惯例。执行以下命令完成部署:
cd /var/www
sudo mkdir ci
sudo chown -R ciuser:www-data ci
sudo -u ciuser wget https://codeload.github.com/bcit-ci/CodeIgniter/zip/refs/tags/3.1.13 -O ci.zip
sudo -u ciuser unzip ci.zip -d /tmp/
sudo -u ciuser cp -r /tmp/CodeIgniter-3.1.13/* /var/www/ci/
sudo -u ciuser rm -rf /tmp/CodeIgniter-3.1.13 /tmp/ci.zip
注意
sudo -u ciuser
确保所有操作以部署用户身份进行,避免后续权限混乱。此时访问
http://你的VPS_IP/ci/
,应该看到经典的CodeIgniter欢迎页——但这只是万里长征第一步,真正的MVC理解,从修改
routes.php
开始。
3. 核心机制深度拆解:Routing、Controller、Model、View的协同逻辑与实操陷阱
3.1 Routing:URL如何被翻译成PHP函数调用?从
index.php
到
Welcome.php
的完整链路
CodeIgniter的路由机制,本质是一场URL字符串的“模式匹配游戏”。当你在浏览器输入
http://vps_ip/ci/index.php/welcome
,Apache先根据
.htaccess
规则把请求转发给
index.php
(这是CodeIgniter的单一入口模式),然后
index.php
读取
application/config/routes.php
,逐行匹配
$route
数组。很多人以为
$route['default_controller'] = 'welcome';
只是设置首页,其实它定义了整个URI解析的默认行为:当URL中没有显式指定控制器时,CodeIgniter自动补上
welcome
。而
$route['(:any)'] = 'welcome/$1';
这种通配规则,则是把所有未匹配的URL都交给
Welcome
控制器处理。
但这里有个致命陷阱:
Apache的
mod_rewrite
模块必须启用,且
.htaccess
文件必须放在
/var/www/ci/
根目录下
。新手常犯的错是把
.htaccess
放在
/var/www/ci/application/
里,或者忘记启用重写模块。验证方法很简单:在
/var/www/ci/
下创建
.htaccess
文件,内容为:
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php/$1 [L]
然后执行
sudo a2enmod rewrite
启用模块,再编辑
/etc/apache2/sites-enabled/000-default.conf
,在
<Directory /var/www/>
区块内添加:
AllowOverride All
Require all granted
最后
sudo systemctl restart apache2
。如果不做这步,你永远只能用
http://vps_ip/ci/index.php/welcome
这种带
index.php
的丑陋URL,而
http://vps_ip/ci/welcome
会返回404。
更深层的理解在于:CodeIgniter的URI分段(Segments)机制。
http://vps_ip/ci/product/list/123
被拆成
[product, list, 123]
三个段,
product
是控制器名,
list
是方法名,
123
是参数。这个拆分发生在
system/core/URI.php
的
_parse_uri()
方法里,它用
explode('/', trim($uri, '/'))
暴力分割。所以,如果你在路由里写
$route['products/(:num)'] = 'product/view/$1';
,CodeIgniter会把
/products/123
重写为
/product/view/123
,然后调用
Product
控制器的
view()
方法,并把
123
作为第一个参数传入。这就是路由的魔法——它不改变URL,只改变内部调用逻辑。
3.2 Controller:不只是“写业务逻辑的地方”,而是HTTP请求的调度中心
CodeIgniter的控制器(Controller)文件必须继承
CI_Controller
基类,且文件名首字母大写(
Welcome.php
),类名与文件名严格一致(
class Welcome extends CI_Controller
)。这个约定不是为了好看,而是为了
system/core/Loader.php
里的自动加载机制:当CodeIgniter解析出控制器名为
welcome
,它会自动转换为
Welcome
,然后去
application/controllers/
目录下找
Welcome.php
文件。如果文件名是
welcome.php
(小写),加载就会失败,报错
Unable to load your default controller.
。
控制器的核心职责有三:
第一,接收并过滤输入
。永远不要直接用
$_GET['id']
,而要用
$this->input->get('id')
。这个方法会自动调用
xss_clean()
做过滤,防止跨站脚本攻击。比如用户提交
<script>alert(1)</script>
,
$this->input->get()
会把它转义为
<script>alert(1)</script>
。
第二,调用模型处理数据
。控制器本身不碰数据库,只负责“发号施令”。比如
$data['products'] = $this->product_model->get_all();
,这里
$this->product_model
是通过
$this->load->model('product_model')
动态加载的,CodeIgniter会在
application/models/
下找
Product_model.php
(注意命名规则:文件名小写,类名首字母大写)。
第三,传递数据给视图
。用
$this->load->view('product/list', $data)
,其中
$data
是一个关联数组,键名会自动转为视图文件中的变量名。
$data['products']
在
application/views/product/list.php
里就直接是
$products
变量。
一个典型陷阱是:
控制器方法名不能以下划线开头
。CodeIgniter会把以下划线开头的方法(如
_init_config()
)视为私有方法,禁止从URL直接访问。这是框架内置的安全机制,防止敏感方法被恶意调用。
3.3 Model:数据库操作的“安全围栏”,为什么
$this->db->query()
不如
$this->db->get()
安全
CodeIgniter的模型(Model)不是必须的,但强烈建议使用。它的价值在于把数据库操作逻辑从控制器里剥离,形成可复用、可测试的单元。创建模型很简单:在
application/models/
下新建
Product_model.php
,内容为:
<?php
class Product_model extends CI_Model {
public function __construct() {
parent::__construct();
$this->load->database(); // 显式加载数据库,避免隐式加载的不确定性
}
public function get_all() {
return $this->db->get('products')->result();
}
public function get_by_id($id) {
$this->db->where('id', $id);
return $this->db->get('products')->row();
}
}
注意两个关键点:
第一,
$this->load->database()
必须在构造函数里显式调用。CodeIgniter支持自动加载数据库(在
application/config/autoload.php
里设
$autoload['libraries'] = array('database');
),但显式调用更可控,避免因配置错误导致
$this->db
为空对象。
第二,
$this->db->get('products')
比
$this->db->query("SELECT * FROM products")
安全得多。前者是查询构建器(Query Builder)模式,CodeIgniter会自动为
products
表名加上反引号(
`products`
),防止SQL注入;而后者是原生查询,如果
$id
来自用户输入且未过滤,
$this->db->query("SELECT * FROM products WHERE id = $id")
就是典型的注入漏洞。正确做法是用绑定参数:
$this->db->query("SELECT * FROM products WHERE id = ?", array($id))
。
更深层的原理是:CodeIgniter的数据库类在
system/database/DB_driver.php
里实现了
escape()
方法,它会对所有传入的字符串参数调用
mysql_real_escape_string()
(MySQLi驱动下)或
pg_escape_string()
(PostgreSQL下),而原生SQL字符串不会触发这个保护。
3.4 View:模板引擎的“最后一道防线”,为什么
<?php echo $name; ?>
比
<?= $name ?>
更安全
CodeIgniter的视图(View)本质就是PHP文件,但它遵循严格的“只负责展示,不处理逻辑”原则。
application/views/welcome_message.php
里,你看到的是纯HTML混着
<?php echo $heading; ?>
这样的输出语句。这里有个重要细节:
永远用
<?php echo
而不是短标签
<?=
。因为短标签在部分PHP配置中是关闭的(
short_open_tag = Off
),而CodeIgniter默认不启用短标签支持。如果你写了
<?= $heading ?>
,页面会直接显示
<?php echo $heading; ?>
的原始代码,而不是渲染内容。
视图安全的核心是“输出过滤”。CodeIgniter提供了
html_escape()
辅助函数:
<?php echo html_escape($user_input); ?>
。这个函数会把
<
转为
<
,
>
转为
>
,
"
转为
"
,从而防止XSS攻击。但要注意,它只对字符串有效,对数组或对象会返回空。所以最佳实践是:在控制器里就做好过滤,再传给视图。比如:
$data['title'] = html_escape($this->input->post('title'));
$this->load->view('product/edit', $data);
这样视图里直接
<?php echo $title; ?>
就是安全的。如果在视图里再套一层
html_escape()
,属于重复劳动,还可能因多次转义导致显示异常(如
&lt;
)。
另一个易忽略的点是:
视图文件不能有PHP短标签以外的语法糖
。CodeIgniter不支持Blade模板的
@foreach
、Twig的
{% for %}
,它就是原生PHP。所以
<?php foreach($products as $p): ?>
是合法的,但
@foreach($products as $p)
会直接报错
Parse error: syntax error, unexpected '@'
。这看似是缺点,实则是优点——没有学习成本,所有PHP开发者都能立刻上手。
4. 实战项目:基于MVC架构的商品管理系统搭建全流程
4.1 数据库设计与初始化:从MySQL建表到CodeIgniter迁移脚本
商品管理系统的核心表只有两张:
products
(商品主表)和
categories
(分类表)。在VPS上用MySQL命令行创建:
mysql -u root -p
CREATE DATABASE ci_shop CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE ci_shop;
CREATE TABLE categories (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
slug VARCHAR(100) UNIQUE NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE products (
id INT AUTO_INCREMENT PRIMARY KEY,
category_id INT NOT NULL,
name VARCHAR(200) NOT NULL,
description TEXT,
price DECIMAL(10,2) NOT NULL DEFAULT 0.00,
stock INT NOT NULL DEFAULT 0,
image_url VARCHAR(255),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE CASCADE
);
INSERT INTO categories (name, slug) VALUES ('Electronics', 'electronics'), ('Books', 'books');
INSERT INTO products (category_id, name, description, price, stock) VALUES
(1, 'Wireless Headphones', 'Noise-cancelling Bluetooth headphones', 129.99, 50),
(2, 'PHP Programming Guide', 'Comprehensive guide to modern PHP', 39.99, 100);
注意
utf8mb4
字符集,这是存储Emoji和生僻汉字的必备选项,
utf8
在MySQL里实际是
utf8mb3
,不支持4字节Unicode。
ON DELETE CASCADE
确保删除分类时,其下商品自动清除,避免外键冲突。
CodeIgniter的数据库迁移(Migration)功能在3.x版本中是实验性的,我们采用更稳妥的手动SQL初始化。在
application/config/database.php
里配置数据库连接:
$db['default'] = array(
'dsn' => '',
'hostname' => 'localhost',
'username' => 'ciuser',
'password' => 'your_strong_password',
'database' => 'ci_shop',
'dbdriver' => 'mysqli',
'dbprefix' => '',
'pconnect' => FALSE,
'db_debug' => TRUE, // 开发期开启,上线后改为FALSE
'cache_on' => FALSE,
'cachedir' => '',
'char_set' => 'utf8mb4',
'dbcollat' => 'utf8mb4_unicode_ci',
'swap_pre' => '',
'encrypt' => FALSE,
'compress' => FALSE,
'stricton' => FALSE,
'failover' => array(),
'save_queries' => TRUE
);
关键参数是
char_set
和
dbcollat
,必须与MySQL建表时一致,否则中文存入后显示为
????
。
db_debug
设为
TRUE
能让错误信息直接显示在页面上,方便调试,但上线前必须关掉,否则会暴露数据库结构等敏感信息。
4.2 Controller开发:
Product
控制器的CRUD全实现与RESTful风格适配
创建
application/controllers/Product.php
:
<?php
defined('BASEPATH') OR exit('No direct script access allowed');
class Product extends CI_Controller {
public function __construct() {
parent::__construct();
$this->load->model('product_model');
$this->load->library('form_validation'); // 加载表单验证库
$this->load->helper('url_helper'); // 加载URL辅助函数
}
// 列表页:GET /product
public function index() {
$data['products'] = $this->product_model->get_all_with_category();
$this->load->view('product/index', $data);
}
// 详情页:GET /product/view/123
public function view($id = NULL) {
if (empty($id)) {
show_404();
}
$data['product'] = $this->product_model->get_by_id($id);
if (empty($data['product'])) {
show_404();
}
$this->load->view('product/view', $data);
}
// 创建页:GET /product/create
public function create() {
$this->load->view('product/create');
}
// 处理创建:POST /product/create
public function store() {
$this->form_validation->set_rules('name', 'Product Name', 'required|min_length[3]|max_length[200]');
$this->form_validation->set_rules('price', 'Price', 'required|numeric|greater_than[0]');
$this->form_validation->set_rules('stock', 'Stock', 'required|integer|greater_than_equal_to[0]');
if ($this->form_validation->run() === FALSE) {
$this->load->view('product/create');
} else {
$data = array(
'category_id' => $this->input->post('category_id'),
'name' => $this->input->post('name'),
'description' => $this->input->post('description'),
'price' => $this->input->post('price'),
'stock' => $this->input->post('stock')
);
if ($this->product_model->insert($data)) {
$this->session->set_flashdata('success', 'Product created successfully!');
redirect('product');
} else {
$this->session->set_flashdata('error', 'Failed to create product.');
$this->load->view('product/create');
}
}
}
// 编辑页:GET /product/edit/123
public function edit($id = NULL) {
if (empty($id)) {
show_404();
}
$data['product'] = $this->product_model->get_by_id($id);
if (empty($data['product'])) {
show_404();
}
$this->load->view('product/edit', $data);
}
// 处理编辑:POST /product/update/123
public function update($id = NULL) {
if (empty($id)) {
show_404();
}
$this->form_validation->set_rules('name', 'Product Name', 'required|min_length[3]|max_length[200]');
$this->form_validation->set_rules('price', 'Price', 'required|numeric|greater_than[0]');
$this->form_validation->set_rules('stock', 'Stock', 'required|integer|greater_than_equal_to[0]');
if ($this->form_validation->run() === FALSE) {
$data['product'] = $this->product_model->get_by_id($id);
$this->load->view('product/edit', $data);
} else {
$data = array(
'category_id' => $this->input->post('category_id'),
'name' => $this->input->post('name'),
'description' => $this->input->post('description'),
'price' => $this->input->post('price'),
'stock' => $this->input->post('stock')
);
if ($this->product_model->update($id, $data)) {
$this->session->set_flashdata('success', 'Product updated successfully!');
redirect('product');
} else {
$this->session->set_flashdata('error', 'Failed to update product.');
$data['product'] = $this->product_model->get_by_id($id);
$this->load->view('product/edit', $data);
}
}
}
// 删除:POST /product/delete/123
public function delete($id = NULL) {
if (empty($id)) {
show_404();
}
if ($this->product_model->delete($id)) {
$this->session->set_flashdata('success', 'Product deleted successfully!');
} else {
$this->session->set_flashdata('error', 'Failed to delete product.');
}
redirect('product');
}
}
这段代码体现了CodeIgniter的典型MVC协作:
-
构造函数里
$this->load->model()加载模型,$this->load->library()加载验证库,这是CodeIgniter的依赖注入雏形。 -
form_validation库的规则链式调用(set_rules())非常直观,min_length[3]这种语法比手写strlen($_POST['name']) < 3简洁十倍。 -
show_404()是CodeIgniter内置的404页面函数,比header('HTTP/1.0 404 Not Found')更专业,会自动加载application/errors/error_404.php模板。 -
redirect('product')是URL重定向,它会生成Location: http://vps_ip/ci/product响应头,比header('Location: ...')更安全,自动处理相对路径。
4.3 Model增强:支持关联查询、软删除与事务回滚的健壮实现
application/models/Product_model.php
需要升级,支持从
products
表关联查出分类名称:
<?php
class Product_model extends CI_Model {
public function __construct() {
parent::__construct();
$this->load->database();
}
// 获取所有商品及分类名称
public function get_all_with_category() {
$this->db->select('p.id, p.name, p.price, p.stock, p.created_at, c.name as category_name');
$this->db->from('products p');
$this->db->join('categories c', 'p.category_id = c.id', 'left');
$this->db->order_by('p.created_at', 'desc');
$query = $this->db->get();
return $query->result();
}
// 根据ID获取商品(含分类)
public function get_by_id($id) {
$this->db->select('p.*, c.name as category_name');
$this->db->from('products p');
$this->db->join('categories c', 'p.category_id = c.id', 'left');
$this->db->where('p.id', $id);
$query = $this->db->get();
return $query->row();
}
// 插入新商品
public function insert($data) {
return $this->db->insert('products', $data);
}
// 更新商品
public function update($id, $data) {
$this->db->where('id', $id);
return $this->db->update('products', $data);
}
// 软删除:标记deleted_at时间戳,而非物理删除
public function soft_delete($id) {
$this->db->where('id', $id);
return $this->db->update('products', array('deleted_at' => date('Y-m-d H:i:s')));
}
// 物理删除(慎用)
public function delete($id) {
$this->db->trans_start(); // 开启事务
$this->db->where('id', $id);
$result = $this->db->delete('products');
$this->db->trans_complete(); // 提交或回滚
return $this->db->trans_status();
}
}
关键增强点:
-
关联查询
:
$this->db->join()方法生成LEFT JOIN SQL,比手写SELECT * FROM products p LEFT JOIN categories c ON p.category_id = c.id更安全,自动处理表名转义。 -
软删除
:新增
deleted_at字段,在products表里加ALTER TABLE products ADD COLUMN deleted_at TIMESTAMP NULL;。软删除避免数据丢失,符合审计要求。 -
事务支持
:
$this->db->trans_start()和$this->db->trans_complete()包裹数据库操作,如果中间某步失败(如插入日志表失败),整个事务会自动回滚,保证数据一致性。这是电商系统库存扣减的必备能力。
4.4 View模板:Bootstrap 5集成与AJAX无刷新交互实战
application/views/product/index.php
使用Bootstrap 5构建响应式表格:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>商品管理</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-4">
<h1 class="mb-4">商品列表</h1>
<a href="<?php echo site_url('product/create'); ?>" class="btn btn-primary mb-3">添加商品</a>
<?php if ($this->session->flashdata('success')): ?>
<div class="alert alert-success"><?php echo $this->session->flashdata('success'); ?></div>
<?php endif; ?>
<?php if ($this->session->flashdata('error')): ?>
<div class="alert alert-danger"><?php echo $this->session->flashdata('error'); ?></div>
<?php endif; ?>
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>ID</th>
<th>商品名称</th>
<th>分类</th>
<th>价格</th>
<th>库存</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<?php foreach($products as $product): ?>
<tr>
<td><?php echo html_escape($product->id); ?></td>
<td><?php echo html_escape($product->name); ?></td>
<td><?php echo html_escape($product->category_name); ?></td>
<td>¥<?php echo html_escape($product->price); ?></td>
<td><?php echo html_escape($product->stock); ?></td>
<td>
<a href="<?php echo site_url('product/view/' . $product->id); ?>" class="btn btn-sm btn-info">查看</a>
<a href="<?php echo site_url('product/edit/' . $product->id); ?>" class="btn btn-sm btn-warning">编辑</a>
<button type="button" class="btn btn-sm btn-danger" onclick="confirmDelete(<?php echo $product->id; ?>)">删除</button>
</td>
</tr>
<?php endforeach; ?>

197

被折叠的 条评论
为什么被折叠?



