VPS上手敲CodeIgniter:从零掌握PHP MVC架构

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() 会把它转义为 &lt;script&gt;alert(1)&lt;/script&gt;
第二,调用模型处理数据 。控制器本身不碰数据库,只负责“发号施令”。比如 $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); ?> 。这个函数会把 < 转为 &lt; > 转为 &gt; " 转为 &quot; ,从而防止XSS攻击。但要注意,它只对字符串有效,对数组或对象会返回空。所以最佳实践是:在控制器里就做好过滤,再传给视图。比如:

$data['title'] = html_escape($this->input->post('title'));  
$this->load->view('product/edit', $data);  

这样视图里直接 <?php echo $title; ?> 就是安全的。如果在视图里再套一层 html_escape() ,属于重复劳动,还可能因多次转义导致显示异常(如 &amp;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; ?>  
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值