第八次课课堂笔记:查看预约信息(完整代码 + 代码讲解)
一、本节课要完成什么功能
这一节课要实现的是:查看当前用户已经预约了哪些票。
完整流程是:
客户端发送请求 type="CK" 和 user_tel
→ 服务器收到请求
→ 服务器连接数据库
→ 查询这个用户预约过哪些票
→ 把票号和票名整理成 JSON 返回
→ 客户端解析 JSON
→ 把预约信息显示出来,并保存本地映射关系。
这一节课已经不只是"请求 + 返回 OK/ERR"了,而是进入了真正的业务查询阶段 。
也就是:服务器开始返回真正的数据给客户端,而不是只返回一个状态。
二、本节课涉及的三个核心函数
这一节课最关键的是三个函数:
-
客户端函数
Client::Show_Yd_info()负责发送请求、接收服务器响应、解析 JSON、显示预约结果。
-
服务器业务函数
Recv_CallBack::Show_Yd_info()负责接收客户端请求、取出手机号、创建数据库对象、调用数据库函数、把结果发回客户端。
-
数据库函数
Mysql_Client::Query_Read_Yd_Info()负责拼 SQL、执行查询、读取结果集、把结果整理成 JSON。
三、客户端完整代码 + 讲解
1)客户端完整代码
cpp
void Client::Show_Yd_info()
{
Json::Value val;
val["type"] = "CK";
val["user_tel"] = curr_usertel;
send(sockfd, val.toStyledString().c_str(),
strlen(val.toStyledString().c_str()), 0);
char buff[4096] = {0};
int n = recv(sockfd, buff, 4095, 0);
if (n <= 0)
{
cout << "ser close" << endl;
return;
}
cout << buff << endl;
Json::Value res_val;
Json::Reader Read;
if (!Read.parse(buff, res_val))
{
cout << "json err" << endl;
return;
}
string status_str = res_val["status"].asString();
if (status_str.compare("OK") != 0)
{
cout << "查询失败" << endl;
return;
}
int num = res_val["num"].asInt();
cout << "你共有:" << num << " 条已预约的信息" << endl;
if (!res_val["yd_arr"].isArray())
{
cout << "无法显示该用户预约的信息" << endl;
return;
}
my_yd_map.clear();
cout << "----------------------" << endl;
cout << "--编号-- --预约地点名称--" << endl;
int arr_size = res_val["yd_arr"].size();
for (int i = 0; i < arr_size; i++)
{
int tk_id = res_val["yd_arr"][i]["ticket_id"].asInt();
string tk_name = res_val["yd_arr"][i]["ticket_name"].asString();
my_yd_map.insert(make_pair(i, tk_id));
cout << "| " << i << " " << tk_name << endl;
}
}
这段代码就是客户端"查看预约信息"的完整处理逻辑。
2)客户端代码逐段讲解
第一步:封装请求 JSON
cpp
Json::Value val;
val["type"] = "CK";
val["user_tel"] = curr_usertel;
这一段的作用是告诉服务器两件事:
-
type = "CK":这次请求是"查看预约信息" -
user_tel = curr_usertel:要查询的是当前登录用户的手机号
这里的 curr_usertel 很关键,因为服务器必须根据这个手机号去数据库查"这个人预约了哪些票"。
第二步:把请求发给服务器
cpp
send(sockfd, val.toStyledString().c_str(),
strlen(val.toStyledString().c_str()), 0);
这里和登录请求是一样的思路:
先把 JSON 转成字符串,再通过 socket 发给服务器。
第三步:接收服务器返回的数据
cpp
char buff[4096] = {0};
int n = recv(sockfd, buff, 4095, 0);
if (n <= 0)
{
cout << "ser close" << endl;
return;
}
这里 buff 开到 4096,是因为这次服务器返回的不再只是简单的:
cpp
{"status":"OK"}
而是可能带有一个数组 yd_arr,如果用户预约了多张票,返回内容会更长,所以缓冲区要开大一点。
n <= 0 说明没有正常收到数据,要么服务器断开了,要么连接有问题。
所以这里先做了一个最基本的连接判断。
第四步:先打印原始字符串
cpp
cout << buff << endl;
这一句主要是调试用的。
意义是先看服务器原始到底回了什么。
因为如果后面 JSON 解析失败,你就能先检查是不是服务器发回来的原始字符串本身就有问题。
第五步:把字符串反序列化成 JSON
cpp
Json::Value res_val;
Json::Reader Read;
if (!Read.parse(buff, res_val))
{
cout << "json err" << endl;
return;
}
这里做的是JSON 反序列化。
服务器发回来的是字符串,客户端想继续处理的话,必须把它重新变成 JSON 对象,这样后面才能访问:
-
res_val["status"] -
res_val["num"] -
res_val["yd_arr"]
第六步:先判断查询是否成功
cpp
string status_str = res_val["status"].asString();
if (status_str.compare("OK") != 0)
{
cout << "查询失败" << endl;
return;
}
这一段体现了一个很重要的思想:客户端不能盲目信任返回数据,要先判断状态。
只有当 status == "OK" 时,才继续往下解析预约数组。
如果不是 OK,说明服务器查库失败,或者服务端直接返回了错误结果。
第七步:读取预约条数
cpp
int num = res_val["num"].asInt();
cout << "你共有:" << num << " 条已预约的信息" << endl;
num 表示服务器查到了多少条预约记录。
这个字段的作用是先给用户一个总提示:你一共有几条预约信息。
第八步:判断 yd_arr 是否真的是数组
cpp
if (!res_val["yd_arr"].isArray())
{
cout << "无法显示该用户预约的信息" << endl;
return;
}
这里体现的是防御式编程。
因为客户端不能想当然地认为服务器返回的一定是数组。
先检查一下格式,可以避免很多奇怪的 bug。
第九步:清空本地映射表
cpp
my_yd_map.clear();
这一步特别重要。
因为用户可能不是第一次点"查看预约信息"。
如果不清空,之前查询留下的数据会残留,后面再做"取消预约"之类操作时,可能就会用到错误的映射。
第十步:打印表头
cpp
cout << "----------------------" << endl;
cout << "--编号-- --预约地点名称--" << endl;
这一步就是让显示更友好。
不再是直接把 JSON 打出来,而是开始像一个小界面那样把结果展示给用户。
第十一步:遍历预约数组
cpp
int arr_size = res_val["yd_arr"].size();
for (int i = 0; i < arr_size; i++)
{
int tk_id = res_val["yd_arr"][i]["ticket_id"].asInt();
string tk_name = res_val["yd_arr"][i]["ticket_name"].asString();
my_yd_map.insert(make_pair(i, tk_id));
cout << "| " << i << " " << tk_name << endl;
}
这里就是客户端真正读取预约信息的核心。
每一条预约记录中都有两个重要字段:
-
ticket_id -
ticket_name
客户端一边显示 ticket_name,一边把 i -> tk_id 存到 my_yd_map 里。
第十二步:my_yd_map 的真实作用
cpp
my_yd_map.insert(make_pair(i, tk_id));
这一步非常重要,它不是多余代码。
它保存的是:
显示编号 → 真实票号
比如屏幕上显示:
-
0 图书馆 -
1 历史博物馆
但数据库里的真实票号可能是:
-
0 -> 2 -
1 -> 1
这样以后如果用户做"取消预约",只输入显示编号,程序就能通过 my_yd_map 找到真正的 ticket_id。
所以这个 map 是在为后续功能做准备。
四、服务器端完整代码 + 讲解
1)服务器业务函数完整代码
cpp
void Recv_CallBack::Show_Yd_info()
{
string user_tel = val["user_tel"].asString();
cout << "Show Yd: tel:" << user_tel << endl;
Mysql_Client mysqlcli;
if (!mysqlcli.Init())
{
Send_err();
return;
}
if (!mysqlcli.Connect_mysql_ser())
{
Send_err();
return;
}
Json::Value res_val;
if (!mysqlcli.Query_Read_Yd_Info(user_tel, res_val))
{
Send_err();
return;
}
send(c, res_val.toStyledString().c_str(),
strlen(res_val.toStyledString().c_str()), 0);
}
这段代码是服务器端"查看预约信息"的业务函数。
2)服务器业务函数讲解
先从客户端请求里取手机号
cpp
string user_tel = val["user_tel"].asString();
这一句表示:
服务器先拿到客户端发来的手机号,因为后面数据库查询必须知道"要查谁"。
创建数据库对象
cpp
Mysql_Client mysqlcli;
后面的数据库初始化、连接、执行 SQL,都交给这个数据库对象处理。
这说明服务器业务函数本身不直接堆复杂数据库代码,而是通过数据库类去完成。
初始化数据库
cpp
if (!mysqlcli.Init())
{
Send_err();
return;
}
先初始化数据库环境。
如果初始化失败,就直接返回错误。
连接数据库
cpp
if (!mysqlcli.Connect_mysql_ser())
{
Send_err();
return;
}
连接数据库服务端。
连接失败就没法继续查数据,所以要立即返回错误。
调用数据库查询函数
cpp
Json::Value res_val;
if (!mysqlcli.Query_Read_Yd_Info(user_tel, res_val))
{
Send_err();
return;
}
这是这一节课最关键的一步。
服务器业务函数不自己写 SQL,而是调用数据库类里的专门函数去查。
这体现了这节课最重要的设计思想:
业务函数负责流程,数据库函数负责查数据。
把整理好的 JSON 发回客户端
cpp
send(c, res_val.toStyledString().c_str(),
strlen(res_val.toStyledString().c_str()), 0);
数据库函数已经把结果整理成了 JSON,服务器这里直接发回客户端即可。
五、数据库函数完整代码 + 讲解
1)数据库函数完整代码
cpp
bool Mysql_Client::Query_Read_Yd_Info(const string& tel, Json::Value &res_val)
{
string sql = string("select ticket_table.ticket_id, ticket_table.ticket_name "
"from ticket_table inner join yd_ticket "
"on ticket_table.ticket_id = yd_ticket.ticket_id "
"where yd_ticket.tel_id = ") + tel;
if (mysql_query(&mysql_con, sql.c_str()) != 0)
{
return false;
}
MYSQL_RES *r = mysql_store_result(&mysql_con);
if (r == NULL)
{
return false;
}
res_val["status"] = "OK";
int num = mysql_num_rows(r);
if (num == 0)
{
res_val["num"] = 0;
mysql_free_result(r);
return true;
}
for (int i = 0; i < num; i++)
{
MYSQL_ROW row = mysql_fetch_row(r);
Json::Value tmp;
tmp["ticket_id"] = stoi(row[0]);
tmp["ticket_name"] = row[1];
res_val["yd_arr"].append(tmp);
}
mysql_free_result(r);
return true;
}
这就是数据库类里真正查预约信息的函数。
2)数据库函数逐段讲解
第一步:拼 SQL
cpp
string sql = string("select ticket_table.ticket_id, ticket_table.ticket_name "
"from ticket_table inner join yd_ticket "
"on ticket_table.ticket_id = yd_ticket.ticket_id "
"where yd_ticket.tel_id = ") + tel;
这是本节课最重要的 SQL。
它不是查一张表,而是两张表联合查询。
-
ticket_table:保存票的信息 -
yd_ticket:保存用户预约关系
联合查询之后,才能根据手机号查到:
-
这个用户预约过哪些票号
-
这些票号对应什么票名
第二步:执行 SQL
cpp
if (mysql_query(&mysql_con, sql.c_str()) != 0)
{
return false;
}
把 SQL 发给 MySQL 执行。
如果执行失败,函数直接返回 false。
第三步:取结果集
cpp
MYSQL_RES *r = mysql_store_result(&mysql_con);
if (r == NULL)
{
return false;
}
查询成功以后,要把结果集取出来。
如果结果集为空,说明读取失败。
第四步:设置返回状态
cpp
res_val["status"] = "OK";
这里开始组装返回给客户端的 JSON。
说明这次查询是成功的。
第五步:统计查询结果条数
cpp
int num = mysql_num_rows(r);
if (num == 0)
{
res_val["num"] = 0;
mysql_free_result(r);
return true;
}
这一段的意思是:
-
先看查到了几条预约记录
-
如果是 0 条,也不算错误
-
直接告诉客户端:当前用户没有预约信息
这里的 num = 0 是合法结果,不是查询失败。
第六步:循环读取每一条记录
cpp
for (int i = 0; i < num; i++)
{
MYSQL_ROW row = mysql_fetch_row(r);
Json::Value tmp;
tmp["ticket_id"] = stoi(row[0]);
tmp["ticket_name"] = row[1];
res_val["yd_arr"].append(tmp);
}
这是核心中的核心。
每从结果集读一行,就把它变成一个 JSON 对象:
cpp
{
"ticket_id": 1,
"ticket_name": "历史博物馆"
}
然后把它追加到数组 yd_arr 里。
第七步:释放结果集
cpp
mysql_free_result(r);
return true;
结果集用完一定要释放。
否则会造成资源泄漏。
六、本节课最重要的 SQL
cpp
select ticket_table.ticket_id, ticket_table.ticket_name
from ticket_table
inner join yd_ticket
on ticket_table.ticket_id = yd_ticket.ticket_id
where yd_ticket.tel_id = 13900000001;
这条 SQL 是这节课的灵魂。
为什么重要
因为客户端真正想看到的不是:
-
1
-
2
而是:
-
图书馆
-
历史博物馆
单查 yd_ticket 只能知道预约了哪些 ticket_id,
单查 ticket_table 只能知道票本身有哪些信息。
只有把两张表联合起来,才能得到"用户预约了哪些票 + 这些票叫什么名字"。
七、本节课的完整流程图
1. 客户端
用户点击"查看预约信息"
→ 客户端封装:
cpp
{
"type": "CK",
"user_tel": "13900000000"
}
→ 发给服务器。
2. 服务器
收到请求
→ 进入 Recv_CallBack::Show_Yd_info()
→ 取出手机号
→ 初始化数据库
→ 连接数据库
→ 调用 Query_Read_Yd_Info()。
3. 数据库函数
执行联合查询 SQL
→ 取结果集
→ 每条记录整理成:
cpp
{
"ticket_id": ...,
"ticket_name": ...
}
→ 追加到 yd_arr。
4. 服务器返回 JSON
例如:
cpp
{
"status": "OK",
"num": 2,
"yd_arr": [
{
"ticket_id": 2,
"ticket_name": "图书馆"
},
{
"ticket_id": 1,
"ticket_name": "历史博物馆"
}
]
}
然后把它发回客户端。
5. 客户端解析显示
客户端解析 status、num、yd_arr
→ 显示预约条数
→ 显示票名
→ 保存 my_yd_map。
八、本节课知识点
1. 业务函数和数据库函数一般是一对
这是这节课最重要的设计思想:
-
业务函数负责流程
-
数据库函数负责查库
这样代码更清晰。
2. 开始学习返回业务数据
服务器不再只是返回 OK/ERR,而是开始返回真正的业务结果。
3. 学会联合查询
因为预约关系和票信息不在一张表里,所以必须用 inner join。
4. 学会 JSON 数组解析
客户端开始解析:
-
isArray() -
size() -
res_val["yd_arr"][i]["ticket_id"]
这些都很重要。
5. 学会本地映射表的用途
my_yd_map 是为后续"取消预约"做准备的。
九、本节课易错点
1. SQL 只查一张表
那就拿不到票名。
2. 联合条件写错
ticket_table.ticket_id = yd_ticket.ticket_id 写错,查询结果就错。
3. 手机号条件写错
可能查到别人的预约信息。
4. 结果集没循环读取
如果只取一行,多条预约会丢。
5. 结果集没释放
会造成资源泄漏。
6. 客户端只打印 buff 不解析
那功能还停留在调试阶段。
7. 忘记清空 my_yd_map
旧数据残留会影响后续操作。
8. 把显示编号和真实票号混淆
i 只是显示编号,真实票号是 tk_id。
十、这一节课一句话总结
第八次课实现了"查看预约信息"的完整闭环:客户端发送 CK 请求,服务器调用数据库函数执行联合查询,根据手机号查出该用户预约的票号和票名,整理成 JSON 返回;客户端再解析 JSON,显示预约结果,并建立显示编号到真实票号的映射关系。