导读:本期聚焦于剑客创作的《SQLite实战项目:如何用C语言实现餐厅菜单与订单管理系统?》,敬请观看详情。想真正掌握SQLite,动手做一个完整项目是最好的方式。本文以餐厅菜单与订单管理为场景,用C语言配合SQLite3 API,从数据库表结构设计开始,一步步实现菜品增删改查、订单创建、订单明细关联查询、营业额统计等核心功能。文中详细讲解sqlite3_open、sqlite3_exec、sqlite3_prepare_v2等关键接口的使用差异,演示外键约束的开启方法,并给出预处理语句防SQL注入的写法。同时分析了事务批量插入订单明细的性能优化技巧,以及多线程访问数据库时常见的database is locked问题与解决方案,适合想从入门走向实战的开发者参考。

学习SQLite时,很多人的困惑不在于SQL语法本身,而在于不知道如何把它嵌入到一个真实业务里。餐厅的菜单与订单管理恰好是一个麻雀虽小五脏俱全的场景:菜单是典型的主数据,订单是高频写入的流水数据,两者之间还存在一对多的明细关系,几乎覆盖了日常开发中最常见的数据库操作。本文将用C语言加SQLite3官方API,完整实现这个系统,重点讲清楚接口选择、外键处理、防注入和性能优化这几个容易踩坑的地方。

SQLite实战项目:如何用C语言实现餐厅菜单与订单管理系统?

一、数据库设计与建表

先想清楚业务模型再动手建表,是避免后期返工的关键。这个系统至少需要三张表:菜品表menu、订单表orders、订单明细表order_items。菜品表存名称、价格、分类和库存;订单表记录桌号、下单时间和总金额;明细表是订单和菜品之间的桥梁,记录每一道菜的数量和小计。

这里有一个非常经典的坑要提前说明:SQLite默认是不开启外键约束的,即使你在建表语句里写了FOREIGN KEY,删除菜品时也不会阻止脏数据产生。必须在每次打开数据库连接后执行PRAGMA foreign_keys = ON;,外键才会真正生效。很多人在MySQL上养成了习惯,迁移到SQLite后不明所以,就是这个原因。

CREATE TABLE IF NOT EXISTS menu (
    id      INTEGER PRIMARY KEY AUTOINCREMENT,
    name    TEXT NOT NULL UNIQUE,
    price   REAL NOT NULL CHECK(price >= 0),
    category TEXT DEFAULT '家常菜',
    stock   INTEGER DEFAULT 100
);

CREATE TABLE IF NOT EXISTS orders (
    id      INTEGER PRIMARY KEY AUTOINCREMENT,
    table_no INTEGER NOT NULL,
    total   REAL DEFAULT 0,
    created_at TEXT DEFAULT (datetime('now','localtime'))
);

CREATE TABLE IF NOT EXISTS order_items (
    id       INTEGER PRIMARY KEY AUTOINCREMENT,
    order_id INTEGER NOT NULL,
    menu_id  INTEGER NOT NULL,
    qty      INTEGER NOT NULL CHECK(qty > 0),
    subtotal REAL NOT NULL,
    FOREIGN KEY(order_id) REFERENCES orders(id) ON DELETE CASCADE,
    FOREIGN KEY(menu_id)  REFERENCES menu(id)
);

建表语句中用到了CHECK约束和ON DELETE CASCADE,前者可以在数据库层面挡住非法数据,后者让删除订单时自动清掉对应明细,省去应用层手动清理的麻烦。

二、C语言操作SQLite的三个核心接口

SQLite官方C API里最常用的是三个函数:sqlite3_open负责建立连接,sqlite3_exec适合执行不需要参数的批量SQL,sqlite3_prepare_v2配合sqlite3_bind系列函数则用于带参数的增删改查。新手常犯的错误是不管什么场景都用sqlite3_exec拼接字符串,这样不仅麻烦,还存在SQL注入风险。

举个实际的例子:顾客点单时需要插入订单明细。如果用字符串拼接的方式把菜名或ID直接拼进SQL,一旦输入内容里出现单引号,轻则报错,重则被注入攻击。正确的做法是预处理语句加参数绑定,下面的代码演示了添加菜品和创建订单的完整流程。

#include <sqlite3.h>
#include <stdio.h>

static sqlite3 *db = NULL;

int open_db(const char *path) {
    if (sqlite3_open(path, &db) != SQLITE_OK) {
        fprintf(stderr, "打开失败: %s\n", sqlite3_errmsg(db));
        return -1;
    }
    // 关键:开启外键约束,否则FOREIGN KEY形同虚设
    sqlite3_exec(db, "PRAGMA foreign_keys = ON;", 0, 0, 0);
    return 0;
}

// 添加菜品,使用预处理语句防止SQL注入
int add_menu(const char *name, double price) {
    const char *sql = "INSERT INTO menu(name, price) VALUES(?1, ?2);";
    sqlite3_stmt *stmt = NULL;
    if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK)
        return -1;
    sqlite3_bind_text(stmt, 1, name, -1, SQLITE_TRANSIENT);
    sqlite3_bind_double(stmt, 2, price);
    int rc = sqlite3_step(stmt);          // 执行一次
    sqlite3_finalize(stmt);               // 释放语句,必须有
    return (rc == SQLITE_DONE) ? 0 : -1;
}

// 查询并遍历菜单
void list_menu(void) {
    sqlite3_stmt *stmt = NULL;
    sqlite3_prepare_v2(db, "SELECT id, name, price FROM menu ORDER BY id;", -1, &stmt, NULL);
    while (sqlite3_step(stmt) == SQLITE_ROW) {
        printf("%d  %s  %.2f元\n",
               sqlite3_column_int(stmt, 0),
               sqlite3_column_text(stmt, 1),
               sqlite3_column_double(stmt, 2));
    }
    sqlite3_finalize(stmt);
}

注意sqlite3_step的返回值语义:插入更新成功返回SQLITE_DONE,查询有数据返回SQLITE_ROW,这两者不能混用判断。另外sqlite3_finalize必须成对调用,漏掉会造成语句对象泄漏,长时间运行的服务程序会慢慢吃掉内存。

三、订单创建与事务优化

一笔订单往往包含多道菜,意味着要往order_items里插入多条记录,同时还要更新orders的total字段。如果每条INSERT都单独提交一次,SQLite默认的同步机制会让每次写入都经历磁盘刷盘,一笔五道菜的订单就要刷五次盘,速度慢得肉眼可见。

解决办法是用事务把整笔订单包起来。事务的本质是让多条写操作共享一次提交,磁盘只刷一次。在C API层面,就是先执行BEGIN;,全部插入成功后再执行COMMIT;,中间任何一步失败则ROLLBACK;回滚,保证订单数据要么完整写入要么完全不写,这对账务类数据尤为重要。

// 创建一笔订单:items是菜品ID和数量的数组
int create_order(int table_no, int menu_ids[], int qtys[], int count) {
    char *err = NULL;
    sqlite3_exec(db, "BEGIN;", 0, 0, &err);

    // 1. 插入订单主表
    sqlite3_exec(db, "INSERT INTO orders(table_no) VALUES(...);", 0, 0, &err);
    sqlite3_int64 order_id = sqlite3_last_insert_rowid(db);

    // 2. 逐条插入明细并累加总额
    double total = 0;
    const char *sql =
        "INSERT INTO order_items(order_id, menu_id, qty, subtotal) "
        "SELECT ?1, id, ?2, price * ?2 FROM menu WHERE id = ?3;";
    sqlite3_stmt *stmt = NULL;
    sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
    for (int i = 0; i < count; i++) {
        sqlite3_bind_int64(stmt, 1, order_id);
        sqlite3_bind_int(stmt, 2, qtys[i]);
        sqlite3_bind_int(stmt, 3, menu_ids[i]);
        if (sqlite3_step(stmt) != SQLITE_DONE) {
            sqlite3_exec(db, "ROLLBACK;", 0, 0, 0);
            sqlite3_finalize(stmt);
            return -1;
        }
        sqlite3_reset(stmt);   // 重置语句以便复用,比重新prepare快得多
    }
    sqlite3_finalize(stmt);

    // 3. 回写总额并提交
    char buf[128];
    snprintf(buf, sizeof(buf), "UPDATE orders SET total=%.2f WHERE id=%lld;",
             total, (long long)order_id);
    sqlite3_exec(db, buf, 0, 0, &err);
    sqlite3_exec(db, "COMMIT;", 0, 0, &err);
    return 0;
}

上面还有一个细节值得注意:sqlite3_reset可以在同一个语句对象上反复绑定参数执行,省去了反复prepare的开销,批量插入时性能提升明显。而sqlite3_last_insert_rowid则用来拿到自增主键,作为明细表的外键值。

四、统计查询与并发访问的坑

系统跑起来后,老板最关心的是营业额。这类统计查询非常适合用JOIN加聚合函数一条SQL搞定,比如查询某个时间段内销量前十的菜品。

SELECT m.name, SUM(oi.qty) AS sold, SUM(oi.subtotal) AS revenue
FROM order_items oi
JOIN menu m ON m.id = oi.menu_id
JOIN orders o ON o.id = oi.order_id
WHERE o.created_at >= '2024-01-01'
GROUP BY m.id
ORDER BY sold DESC
LIMIT 10;

最后说说部署阶段几乎必然遇到的问题:database is locked。SQLite采用文件级锁,如果收银和后厨两个进程同时写同一个数据库文件,后写的那个就会报这个错。解决办法有几个层次:单进程多线程场景下,开启sqlite3_config(SQLITE_CONFIG_MULTITHREAD)并用同一个连接配合互斥锁串行写入;多进程场景则建议设置忙等待超时sqlite3_busy_timeout(db, 3000),让抢不到锁的一方等待而不是立即失败;写入量更大时,可以考虑WAL模式,执行PRAGMA journal_mode=WAL;后读写可以并行,锁冲突会大幅减少。

整体来看,这个小项目把建表、CRUD、事务、外键、统计查询、并发控制都串了一遍。把这些环节亲手写一遍,比看十遍文档的印象要深刻得多。后续还可以在此基础上加Web界面或对接扫码点单,SQLite单文件、零部署的特性让这些扩展都变得非常轻量。

SQLiteC语言订单管理修改时间:2026-09-13 19:10:57

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/20260913/56190.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。