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

第八次课课堂笔记:查看预约信息(完整代码 + 代码讲解)

一、本节课要完成什么功能

这一节课要实现的是:查看当前用户已经预约了哪些票

完整流程是:

客户端发送请求 type="CK"user_tel

→ 服务器收到请求

→ 服务器连接数据库

→ 查询这个用户预约过哪些票

→ 把票号和票名整理成 JSON 返回

→ 客户端解析 JSON

→ 把预约信息显示出来,并保存本地映射关系。

这一节课已经不只是"请求 + 返回 OK/ERR"了,而是进入了真正的业务查询阶段

也就是:服务器开始返回真正的数据给客户端,而不是只返回一个状态。


二、本节课涉及的三个核心函数

这一节课最关键的是三个函数:

  1. 客户端函数 Client::Show_Yd_info()

    负责发送请求、接收服务器响应、解析 JSON、显示预约结果。

  2. 服务器业务函数 Recv_CallBack::Show_Yd_info()

    负责接收客户端请求、取出手机号、创建数据库对象、调用数据库函数、把结果发回客户端。

  3. 数据库函数 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. 客户端解析显示

客户端解析 statusnumyd_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,显示预约结果,并建立显示编号到真实票号的映射关系。

相关推荐
倔强的石头10610 分钟前
【Linux指南】基础IO系列(八):实战衔接 —— 给微型 Shell 添加完整重定向功能
linux·运维·服务器
try2find14 分钟前
打印ascii码报错问题
java·linux·前端
Ujimatsu1 小时前
虚拟机安装Ubuntu 26.04.x及其常用软件(2026.4)
linux·运维·ubuntu
冰暮流星1 小时前
javascript事件案例-全选框案例
服务器·前端·javascript
一直会游泳的小猫3 小时前
homebrew
linux·mac·工具·包管理
寒秋花开曾相惜4 小时前
(学习笔记)4.2 逻辑设计和硬件控制语言HCL(4.2.1 逻辑门&4.2.2 组合电路和HCL布尔表达式)
linux·网络·数据结构·笔记·学习·fpga开发
狂奔的sherry4 小时前
一次由 mount 引发的 Linux 文件系统“错觉”
linux·运维·服务器
小黑要努力4 小时前
智能音箱遇到的问题(一)
linux·运维·git
ch3nyuyu4 小时前
静态库和动态库的制作
linux·运维·开发语言
一口Linux5 小时前
Linux C编程 | 从0实现telnet获取程序终端控制权
linux·运维·c语言