第九节课总结
第九节课的核心任务,是完成取消预约功能,并在功能完成之后,对当前系统的通信方式、数据库职责、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_tel 和 ticket_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 就像"快递箱子",把 type、user_tel、ticket_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_table 的 count,那数据就乱了。
第二,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_id 和 user_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_id 和 user_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 请求服务器,再由服务器端的另一套管理员业务函数完成处理。从架构统一性、安全性和维护性的角度来看,让管理员端也通过服务器端统一执行业务会更加合理。
十、简短背诵版
-
第九节课核心是完成取消预约功能。
-
客户端先输入显示编号,再通过
my_yd_map找到真正的ticket_id。 -
客户端把
type="QXYY"、user_tel、ticket_id封装成 JSON 发给服务器。 -
服务器通过
Recv_CallBack::Qx_ticket()处理取消预约请求。 -
服务器先判参数,再连数据库,再调用
Query_Qx()。 -
Query_Qx()先删除yd_ticket中的预约关系,再更新ticket_table中的count。 -
这里的
count表示已预约数量,所以取消预约时要减一。 -
客户端和服务器之间使用 JSON 格式的字符串通信。
-
MySQL 的操作统一由服务器端完成。
-
Redis 后续可以用来记录在线状态。
-
管理员端未来更推荐通过 server 统一执行业务
在客户端和服务器端通讯的时候是使用的json格式的字符串通信,字符串是如何生成的呢,每次定义一个json对象,将内容填进去之后序列化成字符串发过去。同样接收之后怎么拿到数据呢,再定义一个json对象把他反序列化为json对象然后再拿到里面的值。

对mysql的操作是通过服务器端实现的。如果想做一些额外的操作,想要关注有多少客户端正在登录中,我们可以引入redis,每当有人登陆时将这个信息往redis中直接set设置。
后面还可以做一个管理员端,和客户端保持一致,有发布功能 ,预约信息,查看用户 登录的用户有多少。
有两种做法 管理员直接操作数据库,如图二

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