服务器预约系统linux小项目-第九节课

第九节课总结

第九节课的核心任务,是完成取消预约功能,并在功能完成之后,对当前系统的通信方式、数据库职责、Redis 扩展方向以及未来管理员端的实现思路做阶段性总结。这一节课不是单纯写一个"删除功能",而是把客户端、服务器端、数据库三层再次串成一条完整业务线。

本节课里,客户端负责让用户输入要取消的编号,通过本地映射表找到真正的 ticket_id,再把取消预约请求封装成 JSON 发给服务器;服务器收到请求后,进入取消预约处理函数,从 JSON 中取出手机号和票号,检查参数是否合法,然后创建数据库对象,初始化并连接数据库,再调用数据库封装函数执行真正的取消预约逻辑;数据库层先删除预约关系表中的记录,再去票表中查询当前的 count,将其减一后更新回数据库。处理成功后,服务器返回 OK,失败返回 ERR,客户端最后根据返回结果提示用户取消成功或失败。

在完成这个功能之后,本节课又顺带总结了当前整个项目的工作方式:客户端和服务器之间使用的是 JSON 格式的字符串通信 ;MySQL 的所有操作统一由服务器端完成;如果后续想统计当前有哪些用户在线、多少用户已登录,可以引入 Redis 来保存这些临时状态;同时,未来还可以扩展一个 管理员端,用于发布预约信息、查看用户、查看登录情况等功能,而管理员端更推荐通过服务器端统一执行业务,而不是直接操作数据库。

PPT上的一句话总结

第九节课重点:完成取消预约功能,并总结当前系统的 JSON 通信方式、服务器统一操作数据库模式,以及 Redis 和管理员端的扩展思路。


一、本节课在整个项目里的位置

前面你们已经逐步完成了:

  • 注册

  • 登录

  • 查看票信息

  • 预约

  • 查看我的预约信息

那么第九节课继续做的事情,就是把"取消预约 "这一块补完整。

所以现在整个预约系统的主线就变成了:

注册 → 登录 → 查看票 → 预约 → 查看我的预约 → 取消预约

你可以把它理解成一个前台办业务的小系统:

  • 客户端像"前台接待员",负责收用户输入、发请求、显示结果

  • 服务器像"后台审核员",负责判断业务、调数据库

  • 数据库像"档案室",真正保存用户信息、票信息和预约关系

这一节课的意义就在于:
系统已经不只是能预约了,还能把预约取消掉,业务闭环更完整了。


二、本节课的完整流程

先不要急着看代码,先把整条流程记住。

因为你是小白,所以一定要先知道"整件事是怎么跑起来的",再去看每一段代码。

取消预约完整流程

客户端

用户输入要取消的编号

→ 客户端用 my_yd_map 找到真正的 ticket_id

→ 组织 JSON 请求

→ 发给服务器

→ 接收服务器返回结果

→ 解析 status

→ 输出"取消成功"或"取消失败"

服务器端

收到客户端 JSON

→ 解析 type="QXYY"

→ 进入取消预约处理函数

→ 取出 user_telticket_id

→ 判断参数是否合法

→ 创建数据库对象

→ 初始化数据库

→ 连接数据库

→ 调用数据库封装函数 Query_Qx(tel, tk_id)

→ 若成功则返回 OK

→ 若失败则返回 ERR

数据库层

删除预约关系表 yd_ticket 中对应记录

→ 查询 ticket_table 中当前 count

→ 将 count 减一

→ 更新回数据库

这条流程就是本节课最重要的主线。

你后面复习的时候,先能把这条线顺着讲出来,就已经很不错了。


三、客户端代码:取消预约函数

下面开始结合代码讲。

我先放代码,再给你逐段解释。


1. 客户端取消预约函数完整代码(注释版)

cpp 复制代码
void Client::Qx_ticket()
{
// 提示用户输入要取消的"显示编号"
// 注意:这里输入的不是数据库里的真实 ticket_id,
// 而是客户端界面显示给用户看的编号
cout << "请输入要取消的编号:" << endl;
int num = -1;
cin >> num;

// 在本地映射表 my_yd_map 中查找这个编号对应的真实票号
// my_yd_map 的作用是:把"显示编号"映射成"真实的 ticket_id"
map<int,int>::iterator pos = my_yd_map.find(num);

// 如果找不到,说明用户输入的编号无效
// 这时候必须直接返回,不能继续往下执行
// 否则后面就会拿到错误的 ticket_id
if (pos == my_yd_map.end())
{
cout << "输入无效" << endl;
return;
}

// 取出真正的 ticket_id
// 后面发给服务器的必须是真实票号,而不是显示编号
int tk_id = pos->second;

// 定义一个 JSON 对象,用来封装取消预约请求
Json::Value tmpjson;

// type 表示业务类型
// "QXYY" 表示"取消预约"
tmpjson["type"] = "QXYY";

// 把当前登录用户的手机号一起发给服务器
// 服务器需要知道"是谁要取消预约"
tmpjson["user_tel"] = curr_usertel;

// 把真实票号也发给服务器
// 服务器需要知道"取消的是哪一张票"
tmpjson["ticket_id"] = tk_id;

// 把 JSON 对象序列化成字符串
// 因为网络上传输的是字符串,不是 JSON 对象本身
string send_str = tmpjson.toStyledString();

// 通过 socket 发给服务器
send(sockfd, send_str.c_str(), strlen(send_str.c_str()), 0);

// 定义缓冲区,准备接收服务器返回的数据
char status_buff[64] = {0};

// 接收服务器返回的响应报文
int n = recv(sockfd, status_buff, 63, 0);

// 如果 n <= 0,说明服务器关闭了连接或者接收失败
if (n <= 0)
{
cout << "ser close" << endl;
return;
}

// 定义 JSON 对象,用来保存服务器返回的数据
Json::Value res_val;
Json::Reader Read;

// 把收到的字符串反序列化成 JSON 对象
// 如果解析失败,说明返回数据格式不正确
if (!Read.parse(status_buff, res_val))
{
cout << "json err" << endl;
return;
}

// 取出服务器返回的 status 字段
string status_str = res_val["status"].asString();

// 如果 status 不是 OK,说明取消失败
if (status_str.compare("OK") != 0)
{
cout << "取消失败" << endl;
}
else
{
// 如果 status 是 OK,说明取消成功
cout << "取消成功" << endl;
}

return;
}

2. 这段代码整体是在做什么

先用最通俗的话讲:

这段函数就是客户端的"取消预约按钮"。

它做的事情很像你去前台说:"我要取消我刚才约的第 2 个项目。"

前台不会直接拿你嘴里说的"2"去改后台数据库,因为这个 2 只是界面上的编号。

前台会先查一下"第 2 个项目真正对应的是哪个票号",然后把"用户手机号 + 真正票号 + 业务类型"打包成一个 JSON 快递箱子发给服务器。服务器审核通过后,再回一个结果,客户端最后把"成功/失败"告诉你。

所以这段代码的核心任务是:

收用户输入 → 找真实票号 → 封装请求 → 发给服务器 → 接收结果 → 提示用户


3. 为什么要这样写

为什么先查 my_yd_map

因为用户看到的编号不一定等于数据库里的 ticket_id

如果你不查映射表,直接把用户输入的数字发给服务器,就很可能取消错票。

为什么要发 JSON

因为客户端和服务器约定好的通信格式就是 JSON。

JSON 就像"快递箱子",把 typeuser_telticket_id 这些字段统一装进去,服务器收到后才知道你要干什么。

为什么客户端只看 status

因为客户端不应该自己碰数据库。

客户端只负责"发请求"和"看结果"。

真正能不能取消成功,是服务器和数据库一起决定的。


4. 这段代码在整个流程中的作用

这段代码在整个项目里,属于客户端发起取消预约请求的入口

前面它接的是用户输入,后面它把请求交给服务器。

所以它的作用是:把"用户想取消预约"这个想法,变成服务器能处理的正式请求。


5. 这段代码容易出错的地方

第一,用户输入的是显示编号,不是真实票号

如果你忘了用 my_yd_map 转换,就会把错误的票号发给服务器。

第二,JSON 解析可能失败

如果服务器返回的不是合法 JSON,Read.parse() 就会失败。

第三,客户端不能自己判断数据库是否真的取消成功

客户端只能看服务器返回的 status,不能自己猜。


四、数据库封装代码:真正执行取消预约

客户端只是发请求,真正去动数据库的是服务器端调用的数据库函数。

这一段是本节课最关键的数据库逻辑。


1. 数据库封装函数完整代码(注释版)

cpp 复制代码
bool Mysql_Client::Query_Qx(const string& tel, const int &tk_id)
{
// 第一步:删除预约关系表中的记录
// 这一步表示:该用户不再预约这张票了
string sql_del = string("delete from yd_ticket where tel_id=")
+ tel +
string(" and ticket_id=")
+ to_string(tk_id);

// 执行删除 SQL
// 如果执行失败,直接返回 false
if (mysql_query(&mysql_con, sql_del.c_str()) != 0)
{
return false;
}

// 第二步:查询票表中当前的 count
// 因为取消预约之后,还要把票表中的数量同步改回来
string sql_sel = string("select count from ticket_table where ticket_id=")
+ to_string(tk_id);

// 执行查询 SQL
if (mysql_query(&mysql_con, sql_sel.c_str()) != 0)
{
return false;
}

// 取出查询结果集
MYSQL_RES *r = mysql_store_result(&mysql_con);

// 如果结果集为空,说明查询出问题了
if (r == NULL)
{
return false;
}

// 查看查到了几行数据
int num = mysql_num_rows(r);

// 按道理,一个 ticket_id 只能查到一条记录
// 如果不是 1,说明数据库状态异常或者 ticket_id 无效
if (num != 1)
{
cout << "找不到要取消的tk_id,或值无效" << endl;
mysql_free_result(r);
return false;
}

// 取出这一行数据
MYSQL_ROW row = mysql_fetch_row(r);

// row[0] 就是查到的 count
// 把字符串转成 int,方便后面做减法
int count = stoi(row[0]);

// 取消预约后,要把 count 减一
// 这里你们当前的逻辑说明:
// count 表示"当前已预约数量"
if (count >= 1)
{
count--;
}
else
{
// 如果已经是 0,就继续保持 0,避免出现负数
count = 0;
}

// 结果集用完后要释放
mysql_free_result(r);

// 第三步:把新的 count 更新回 ticket_table
string sql_update = string("update ticket_table set count=")
+ to_string(count)
+ string(" where ticket_id=")
+ to_string(tk_id);

// 执行更新 SQL
if (mysql_query(&mysql_con, sql_update.c_str()) != 0)
{
return false;
}

// 前面所有步骤都成功,说明取消预约数据库逻辑完成
return true;
}

2. 这段代码整体是在做什么

这段函数可以理解成后台真正去"改档案"的人。

客户端只是说"我要取消",服务器只是负责"接请求、判对不对",真正把数据库改掉的,是这个函数。

它的核心任务是:

先删除用户和票之间的预约关系,再把票表里的预约数量改回来。

也就是说,它要同时维护两张表的数据一致性:

  • yd_ticket:用户和票的关系表

  • ticket_table:票的信息表


3. 为什么要这样写

为什么先删 yd_ticket

因为取消预约最基本的含义,就是用户不再预约这张票了。

所以必须先把用户和票之间的关系删掉。

为什么还要查 count

因为取消预约不仅影响关系表,还影响票表的统计数据。

如果你只删关系表,不更新 ticket_table 里的 count,数据库状态就不一致了。

为什么不是直接把 count 写死

因为你必须先知道当前 count 是多少,才能算出新的值。

所以流程一定是:

先查旧值 → 再算新值 → 再更新回去

为什么 count 不能减成负数

因为数量逻辑上不可能是负数。

所以必须加判断,最小保持为 0。


4. 这段代码在整个流程中的作用

这段代码处在整个项目最底层,也就是数据库层真正执行业务的地方

前面客户端已经把请求发过来了,服务器也已经判断参数没问题了,最后是不是能取消成功,就看这里。

所以它在整个流程里,起的是"真正落地执行"的作用。


5. 这段代码容易出错的地方

第一,只删预约关系,不更新票表

如果只删 yd_ticket,不改 ticket_tablecount,那数据就乱了。

第二,count 的含义不统一

你们现在代码里的逻辑说明 count 表示"已预约数量"。

后面一定要写死,不要一会儿理解成"剩余票数"。

第三,SQL 拼接时引号问题

如果 tel_id 在表里是字符串类型,SQL 里可能要加引号,这要根据你们表结构统一。

第四,不检查结果集

如果不检查 mysql_num_rows(r),就可能在查不到数据的时候继续往下执行,导致逻辑错误。


五、服务器端代码:连接客户端请求和数据库逻辑

前面客户端负责发请求,数据库函数负责真正改数据。

那中间这一层是谁把两边连起来的?就是服务器端业务函数。


1. 服务器端取消预约函数完整代码(注释版)

cpp 复制代码
void Recv_CallBack::Qx_ticket()
{
// 从客户端发来的 JSON 中取出 ticket_id
int tk_id = val["ticket_id"].asInt();

// 从客户端发来的 JSON 中取出用户手机号
string tel = val["user_tel"].asString();

// 第一步:参数合法性检查
// 如果票号无效,或者手机号为空,直接返回错误
if (tk_id == -1 || tel.empty())
{
Send_err();
return;
}

// 第二步:创建数据库对象
Mysql_Client mysqlcli;

// 第三步:初始化数据库环境
if (!mysqlcli.Init())
{
Send_err();
return;
}

// 第四步:连接数据库
if (!mysqlcli.Connect_mysql_ser())
{
Send_err();
return;
}

// 第五步:调用数据库封装函数执行取消预约逻辑
if (!mysqlcli.Query_Qx(tel, tk_id))
{
Send_err();
mysqlcli.Close();
return;
}

// 第六步:数据库处理成功后,关闭连接
mysqlcli.Close();

// 第七步:给客户端返回成功报文
Send_ok();
return;
}

2. 这段代码整体是在做什么

这段代码相当于"后台审核员"。

客户端把"我要取消预约"的快递箱子发过来以后,服务器先拆开箱子,看里面有没有:

  • 用户手机号

  • 票号

如果这两个信息不正常,就直接告诉客户端"失败"。

如果这两个信息没问题,就去找数据库层,让数据库真正执行取消预约。

所以它的核心任务是:

接收客户端请求 → 检查参数 → 调数据库函数 → 返回结果


3. 为什么要这样写

为什么先判空、判参数

因为如果请求本身就是错的,就没必要再去连接数据库。

这样可以减少无意义的数据库操作。

为什么服务器不直接写 SQL

因为你们前面已经把数据库逻辑封装进 Mysql_Client 类里了。

服务器业务函数只负责流程控制,这样代码层次更清晰。

为什么成功和失败都要及时返回

因为服务器端处理一个业务时,最怕继续往下执行错误流程。

一旦某一步失败,就应该立刻结束并告诉客户端。


4. 这段代码在整个流程中的作用

这段代码正好处在客户端和数据库之间。

前面接客户端发来的 JSON 请求,后面调用数据库函数去改数据。

所以它相当于整条业务线的"中间调度层"。


5. 这段代码容易出错的地方

第一,参数没取对

如果 val["ticket_id"]val["user_tel"] 取值错误,后面整个流程就会错。

第二,失败分支没关闭数据库

你们这里已经在失败时调用了 mysqlcli.Close(),这个意识是对的。

否则可能造成资源泄漏。

第三,客户端和服务器字段名不一致

客户端发的是 ticket_iduser_tel,服务器就必须按这两个字段取值。


六、本节课除了代码,还学到了哪些知识点

这节课不是只会写函数,还学到了很多更底层的理解。


1. 一个业务可能要改两张表

取消预约不是只删一条预约记录。

它至少要做两件事:

  • 删除用户预约关系

  • 更新票表里的数量

这说明业务逻辑经常会影响多张表,而不是只改一处。


2. 客户端显示编号和真实票号不是一回事

用户输入的是界面编号,真正发给服务器的必须是数据库中的 ticket_id

所以 my_yd_map 这个映射表非常关键。


3. JSON 通信模式被再次强化

这节课又把你们项目的通信方式强调了一遍:

发送时:
JSON 对象 → 序列化成字符串 → send

接收时:
recv 收字符串 → 反序列化成 JSON 对象 → 取值

这已经是你们整个项目统一的通信模式了。


4. MySQL 的操作统一由服务器端完成

客户端不直接碰数据库。

客户端负责交互,服务器负责业务,数据库由服务器统一访问。

这是整个项目很重要的一条架构原则。


5. Redis 的引入思路

如果后续想统计:

  • 当前有多少客户端在线

  • 哪些用户已经登录

就可以引入 Redis。

例如用户登录成功后,服务器往 Redis 里写一条状态:

  • key:用户手机号

  • value:已登录

这样 Redis 就适合存这种"变化快的临时状态",而 MySQL 继续存"长期的真实业务数据"。

一句话记住:

MySQL 管长期数据,Redis 管临时状态。


6. 管理员端的扩展思路

未来你们还可以做管理员端,比如支持:

  • 发布预约信息

  • 查看用户

  • 查看当前登录用户数量

实现思路有两种:

第一种,管理员端直接操作数据库。

优点是直接,缺点是耦合太高,不安全。

第二种,管理员端也像普通客户端一样,去连接 server,再由 server 走另外一套管理员业务函数。

这种方式更统一,也更安全。

从架构上看,更推荐第二种。


七、本节课最容易出错的地方

1. 显示编号和真实 ticket_id 混淆

这个是最容易踩的坑。

2. 只删 yd_ticket 不更新 ticket_table

这样数据库状态会不一致。

3. count 可能减成负数

所以必须做判断。

4. count 的业务含义混乱

一定要统一成"已预约数量"。

5. 客户端和服务器字段名不一致

客户端发什么字段,服务器就要按什么字段取。

6. 后面如果管理员端直接连数据库,系统会越来越乱

更推荐让管理员端也通过 server 统一处理业务。


八、面试时怎么说这一节课

面试回答模板

第九节课我们完成了取消预约功能。客户端先让用户输入要取消的显示编号,再通过本地映射表 my_yd_map 转换成真正的 ticket_id,然后把 type=QXYY、用户手机号和票号封装成 JSON 发给服务器。服务器收到请求后,通过 Recv_CallBack::Qx_ticket() 进入取消预约业务函数,先从 JSON 中取出参数并检查是否合法,然后创建数据库对象,完成数据库初始化和连接,再调用封装好的数据库函数 Query_Qx。这个函数会先删除预约关系表 yd_ticket 中对应的记录,再查询票表 ticket_table 中该票当前的 count,将其减一后更新回数据库。最后服务器把处理状态返回给客户端,客户端根据 status 提示取消成功或失败。通过这一节课,我们不仅完成了取消预约功能,还进一步总结了项目中统一的 JSON 通信方式、服务器统一操作 MySQL 的架构,以及后续可以用 Redis 管理在线状态、用管理员端扩展系统功能的思路。


九、详细复习版

第九节课的主要内容,是在前面已经实现注册、登录、查看票信息、预约和查看我的预约信息的基础上,继续完成取消预约功能,并对整个系统当前的通信方式、数据库职责、Redis 引入思路以及管理员端扩展方向做阶段性总结。取消预约功能的实现过程是:客户端先让用户输入要取消的编号,再通过本地映射表 my_yd_map 将显示编号转换成真正的 ticket_id,然后把 type="QXYY"、用户手机号和真实票号封装成 JSON 发给服务器。服务器收到后,通过 Recv_CallBack::Qx_ticket() 进入取消预约业务函数,从 JSON 中取出 ticket_iduser_tel,先检查参数是否合法,再创建 Mysql_Client 数据库对象,完成数据库初始化和连接,最后调用 Query_Qx(tel, tk_id) 执行具体的数据库取消预约逻辑。数据库函数内部会先从预约关系表 yd_ticket 中删除对应记录,再查询票表 ticket_table 中当前的 count,将其减一后更新回数据库。处理成功后,服务器通过 Send_ok() 返回成功状态;若任一步骤失败,则通过 Send_err() 返回失败状态,客户端最后根据服务器返回的 status 输出取消成功或取消失败。

在完成功能之后,本节课又进一步总结了系统当前的工作方式。客户端和服务器之间采用的是 JSON 格式的字符串通信:发送时先构造 JSON 对象,再将其序列化为字符串,通过 socket 发给服务器;接收时先把字符串收下来,再定义 JSON 对象并反序列化,从中取出所需字段。系统中的 MySQL 操作统一由服务器端完成,客户端只负责交互和请求发送,不直接访问数据库。后续如果想统计当前有哪些用户在线、多少客户端已登录,可以引入 Redis,在用户登录成功时将手机号等状态信息写入 Redis,用于保存这些临时状态数据。除此之外,系统未来还可以扩展管理员端,用于发布预约信息、查看用户、查看登录情况等功能。管理员端既可以选择直接操作数据库,也可以像普通客户端一样,通过 JSON 请求服务器,再由服务器端的另一套管理员业务函数完成处理。从架构统一性、安全性和维护性的角度来看,让管理员端也通过服务器端统一执行业务会更加合理。


十、简短背诵版

  1. 第九节课核心是完成取消预约功能。

  2. 客户端先输入显示编号,再通过 my_yd_map 找到真正的 ticket_id

  3. 客户端把 type="QXYY"user_telticket_id 封装成 JSON 发给服务器。

  4. 服务器通过 Recv_CallBack::Qx_ticket() 处理取消预约请求。

  5. 服务器先判参数,再连数据库,再调用 Query_Qx()

  6. Query_Qx() 先删除 yd_ticket 中的预约关系,再更新 ticket_table 中的 count

  7. 这里的 count 表示已预约数量,所以取消预约时要减一。

  8. 客户端和服务器之间使用 JSON 格式的字符串通信。

  9. MySQL 的操作统一由服务器端完成。

  10. Redis 后续可以用来记录在线状态。

  11. 管理员端未来更推荐通过 server 统一执行业务

在客户端和服务器端通讯的时候是使用的json格式的字符串通信,字符串是如何生成的呢,每次定义一个json对象,将内容填进去之后序列化成字符串发过去。同样接收之后怎么拿到数据呢,再定义一个json对象把他反序列化为json对象然后再拿到里面的值。

对mysql的操作是通过服务器端实现的。如果想做一些额外的操作,想要关注有多少客户端正在登录中,我们可以引入redis,每当有人登陆时将这个信息往redis中直接set设置。

后面还可以做一个管理员端,和客户端保持一致,有发布功能 ,预约信息,查看用户 登录的用户有多少。

有两种做法 管理员直接操作数据库,如图二

还有一种 管理员直接连接到server端,server端可以走另外的函数 来实现管理员的工作

相关推荐
AI-小柒2 小时前
大模型API中转推荐:Dataeyes API 600+模型统一网关与负载均衡部署,claude编程、香蕉生图、视频大模型聚合平台
大数据·运维·开发语言·人工智能·算法·机器学习·负载均衡
lulu12165440782 小时前
大模型API中转平台weelinking技术深度解析:架构、性能与部署实践
运维·人工智能·架构·ai编程
Shepherdppz2 小时前
【避坑指南】超级笔记 Supernote 私有云部署完整指南:从零到一在群晖Synology NAS上搭建私人同步服务器
运维·服务器·笔记
智能运维指南2 小时前
嘉为蓝鲸 DevOps 平台与 AI 技术结合:推动数字化转型的行业标杆
运维·人工智能·devops
柴_笔记2 小时前
linux之UDP之组播通信
linux·udp·组播
mingjie12122 小时前
mac virtualbox虚拟机 ubuntu-server openclaw 访问配置
linux·运维·ubuntu·openclaw
藤谷性能2 小时前
Ubuntu 22.04:在双硬盘电脑上安装Ubuntu 22.04单系统
linux·运维·ubuntu
我科绝伦(Huanhuan Zhou)2 小时前
分享一个自己写的智能巡检系统
运维·人工智能·自动化
藤谷性能2 小时前
Ubuntu 22.04:安装后的工作
linux·运维·ubuntu