预约系统版本2
基于 C++ + epoll + pthread + MySQL 的多线程并发预约系统
包含连接池、线程池、epoll I/O 多路复用、JSON 协议、SQL 事务
一、项目概述
1.1 项目定位
一个支持多客户端同时在线的票务预约系统,客户端通过 TCP 长连接与服务端交互,服务端使用 epoll 监听 I/O 事件,并通过线程池把任务交给 worker 线程处理,worker 线程通过连接池借用 MySQL 连接执行 SQL。
1.2 核心特性
| 特性 | 实现方式 |
|---|---|
| 并发模型 | epoll + pthread 线程池 |
| 数据库复用 | Connect_Pool 连接池(5 个连接复用) |
| 协议格式 | JSON(基于 jsoncpp) |
| 事务支持 | 预约/取消操作使用 MySQL 事务 |
| 容错 | 连接池满时返回 "MYSQL CLIENT BUSSY",客户端提示重试 |
1.3 业务功能
- 用户注册(手机号 + 用户名 + 密码)
- 用户登录
- 查看可预约票列表
- 预约票(带事务,防止超卖)
- 查看我的预约
- 取消预约(带事务,保持 count 一致)
二、项目结构
csproject2view/
├── service.h # 服务端头文件(类声明)
├── service.cpp # 服务端实现(主流程 + 业务逻辑)
├── client.h # 命令行客户端头文件
├── client.cpp # 命令行客户端实现
├── service.conf # 服务端配置(IP/Port=9999/lismax)
├── mysql.conf # 数据库配置(IP/Port/db_name)
├── Makefile # 构建脚本
├── service # 编译产物(服务端可执行,监听 9999)
├── client # 编译产物(命令行客户端)
├── start_all.sh # 一键启动(MySQL→C++→Web→浏览器,写 PID 文件)
├── stop_all.sh # 一键停止(PID 文件方式,不误杀其他项目)
├── .service.pid # 启动时记录的 C++ 服务 PID
├── .web.pid # 启动时记录的 Web 服务 PID
├── 预约系统版本2.md # 本文档
├── WEB_GUIDE.md # Web 界面专项说明(可选参考)
└── web/ # ← Web 可视化界面(新增)
├── app.py # Flask 后端(9 个路由 + session 登录态)
├── cp_client.py # C++ TCP 服务的 Python 客户端封装
├── requirements.txt # Python 依赖(flask>=2.0)
├── start.sh # 单独启动 Web 的脚本
├── templates/
│ ├── login.html # 登录 / 注册页
│ └── index.html # 主操作页
└── static/
├── style.css # 页面样式(卡片/阴影/响应式)
└── app.js # 前端逻辑(AJAX、表格渲染、交互)
整体架构
浏览器 (Chrome/Firefox/...)
│ HTTP 请求 (JSON)
▼
Flask Web 服务 (web/app.py, 0.0.0.0:5000)
│ 维持 session 登录态、参数校验、转发
│ TCP 连接到 127.0.0.1:9999
▼
C++ TCP 服务 (service, 127.0.0.1:9999)
│ epoll + 线程池 + MySQL 连接池
▼
MySQL 数据库 (c2411db, 3306)
外部用户只需访问 http://localhost:5000,Web 与 C++ 之间的 9999 端口仅本地可见,不对外暴露。
编译 & 启动
bash
# 方式一:一键启动整个系统(推荐)
./start_all.sh # 自动:检查 MySQL→启动 C++ 服务→装 Flask→启动 Web→开浏览器
# 方式二:仅编译并单独启动 C++ 服务
make clean && make all
./service service.conf
# 方式三:单独启动 Web(调试用,需先确保 C++ 服务在 9999 运行)
cd web && python3 app.py
停止
bash
./stop_all.sh # PID 文件方式精确停止,不误杀其他项目
三、改进建议
3.1 性能改进
3.1.1 连接池加条件变量(当前最大短板)
现状 :Connect_Mysql_Server 借不到连接直接 return false,构造失败 → 客户端收到 BUSSY。
问题 :并发请求超过 5 个时,第 6 个直接被拒,用户体验差。
改进 :在 Connect_Pool 加 pthread_cond_t,借不到时 pthread_cond_wait,等他人归还后唤醒。
cpp
// 借连接
while (C_P_array[i].status == false) pthread_cond_wait(&cond, &mutex);
// 归还时
pthread_cond_signal(&cond);
3.1.2 worker 线程绑定固定连接(彻底消除锁)
现状 :5 个 worker 共享 5 个连接,借/还都要加锁。
改进 :worker 启动时绑死一个连接(线程局部存储 __thread 或 pthread_key),完全无锁。
3.1.3 send/recv 改非阻塞 + 边沿触发
现状 :epoll 用 Level Triggered + EPOLLONESHOT,每次要重新 MOD。
改进:改 Edge Triggered + 非阻塞 fd,循环 recv 到 EAGAIN,减少 epoll_ctl 调用。
3.1.4 SQL 预处理语句
现状 :每次 mysql_query 拼字符串,服务端每次都要 parse SQL。
改进 :用 mysql_stmt_prepare + mysql_stmt_bind_param,复用执行计划。
3.2 安全改进
3.2.1 SQL 注入(必改)
现状 :Db_User_Register、Db_User_Login 等直接拼字符串:
cpp
string sql = "select Name,Passwd from user_info where Tel='" + tel + "'";
风险 :用户 tel 输入 ' OR '1'='1 直接绕过密码校验。
改进 :用 mysql_real_escape_string 转义,或改 prepared statement。
3.2.2 密码明文存储
现状 :Db_User_Register values(0,'tel','name','passwd',...) 直接存明文。
风险 :数据库被拖库后密码全暴露。
改进 :用 crypt()/SHA256/bcrypt 存哈希,登录时比哈希。
3.2.3 客户端不验证服务端身份
现状 :客户端直接 connect,无法识别中间人。
改进:用 TLS(OpenSSL)加密通道。
3.3 健壮性改进
3.3.1 signal 处理
现状 :Ctrl+C 时 epoll_wait 会被 EINTR 中断,但 L895-L900 直接 exit(1),不释放资源。
改进 :注册 SIGINT/SIGTERM handler,设 Status = false,让主循环优雅退出,析构 Tcp_Service → 析构 Thread_Pool → join 所有 worker。
3.3.2 客户端断连感知
现状 :客户端 Ctrl+C 退出,服务端 Do_client 收到 recv_num == 0 走 delete this,OK。但如果客户端网络异常(没发 FIN),服务端不知道。
改进 :用 TCP keepalive(setsockopt SO_KEEPALIVE)或应用层心跳。
3.3.3 EPOLLONESHOT 处理一致
现状 :listen socket 没用 EPOLLONESHOT,client socket 用了。
风险 :多线程同时 accept 同一个 listen fd,可能竞争。
改进:listen fd 也用 EPOLLONESHOT,accept 后重新 MOD。
3.3.4 配置文件硬编码路径
现状 :<service.h#L22> #define Conf_path "/home/xiaoc/mycode/csproject2/service.conf",换机器就废。
改进:从 argv1 读,或读环境变量。
3.3.5 mysql_real_connect 密码硬编码
现状 :Connect_Pool::Run "root","111111" 写死在代码里。
改进 :从 mysql.conf 读用户名/密码,或用 MySQL 配置文件 [client] 段。
3.4 可维护性改进
3.4.1 头文件依赖混乱
现状 :<service.h> 把 Service_Config、Socket、AccSocket、RecvSocket、Thread_Pool、Mysql_config、Mysql_Client、Mysql_cli、Connect_Pool、Tcp_Service 10 个类型全堆在一个文件。
改进 :拆分成 socket.h、thread_pool.h、mysql_pool.h、mysql_client.h、tcp_service.h、service_config.h,每个文件单一职责。
3.4.2 全局变量
现状 :service.cpp#L15-L16 Mysql_config file; 和 Connect_Pool* g_pool; 是全局变量。
问题 :难追踪修改点,难单元测试。
改进 :g_pool 改成 Connect_Pool::GetInstance() 单例;file 放到 Tcp_Service 里作成员。
3.4.3 裸指针 + 手动 new/delete
现状 :Socket* task、Mysql_cli* C_P_array 等全是裸指针,靠 delete 手动管理。
风险 :异常路径漏 delete 会泄漏。
改进 :用 std::unique_ptr / std::shared_ptr,析构自动释放。
3.4.4 错误日志不分级
现状 :所有错误都 cout << 打到 stdout。
改进:引入简单日志库(如 spdlog),分 INFO/WARN/ERROR/FATAL,支持文件分卷。
3.4.5 magic number
现状 :<service.cpp#L787> "root","111111"、<client.h#L18> PORTADRESS 8888、MAXLINE 256、d_size 1024 等硬编码。
改进:全部移到配置文件或 const 常量。
3.5 功能扩展
3.5.1 缺少客户端超时
客户端 recv 是阻塞的,服务端挂了客户端会一直等。
改进 :用 setsockopt SO_RCVTIMEO 设 5 秒超时,超时提示。
3.5.2 缺少管理员后台
现在无法后台管理票务(增删改查票、查用户、统计)。
改进:加一个管理端协议 type=Admin_Login + type=Admin_Add_Ticket 等。
3.5.3 缺少并发数统计
无法查看当前在线用户、池使用率。
改进:服务端加一个 stats 接口(可通过 SIGUSR1 信号 dump 状态到日志)。
四、各文件详解
4.1 <service.h> --- 服务端类声明
完整代码
cpp
#include <stdlib.h>
#include <string>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <jsoncpp/json/json.h>
#include <mysql/mysql.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <pthread.h>
#include <unistd.h>
#define MAXLINE 256
#define START -1
#define Conf_path "/home/xiaoc/mycode/csproject2/service.conf"
#define Sql_Conf_path "/home/xiaoc/mycode/csproject2/mysql.conf"
#define d_size 1024
#define thread_num 5
#define o "ok"
class Connect_Pool;
extern Connect_Pool* g_pool;
struct Service_Config{
const char* config_path=Conf_path;
std::string IP;
short Port;
short Lismax;
void ReadConfig(const char* config_path);
void PrintConfig() const;
};
class Socket{
public:
int fd;
int epfd;
Socket(int fd,int epfd);
virtual ~Socket();
virtual void Do_client() = 0;
};
class AccSocket : public Socket{
public:
AccSocket(int fd,int epfd);
~AccSocket();
void Do_client() override;
private:
static unsigned int count;
};
class RecvSocket : public Socket{
public:
RecvSocket(int fd,int epfd);
~RecvSocket();
void Do_client() override;
void send_Ok();
void send_false();
};
typedef struct{
Socket* task;
}task_t;
class Thread_Pool{
private:
task_t * dequeue;
int dequeue_size;
int front;
int rear;
int task_Num;
int thread_Num;
bool status;
pthread_mutex_t mutex;
pthread_cond_t cond;
pthread_t Thread_array[thread_num];
public:
Thread_Pool();
~Thread_Pool();
bool Init_Thread_pool();
void add_Task(Socket* csocket);
void Create_Thread();
void Work();
};
void* General_Thread(void* arg);
struct Mysql_config{
short db_port;
std::string db_ip;
std::string db_name;
const char* Sql_config_path=Sql_Conf_path;
void Init_Mysql_config(const char* Sql_config_path);
void print_Mysql_config () const;
};
class Mysql_Client{
private:
struct Mysql_config* file;
MYSQL* mysql_con;//mysql的连接句柄
int who;
bool connected; // 是否成功从池里借到连接
public:
Mysql_Client(struct Mysql_config * Sql_config,int who);
void ResetSTatus();
~Mysql_Client();
bool IsConnected() const { return connected; } // 供外部检查是否借到连接
bool Connect_Mysql_Server();
bool Db_User_Register(const std::string &user_tel,const std::string &user_name,const std::string &user_passwd);
bool Db_User_Login(const std::string& tel,std::string& name,const std::string& passwd);
bool Db_Show_Ticket(Json::Value &res);
bool Db_Yd_Ticket(std::string usertel,std::string tk_id);
bool Db_Show_My_Ticket(const std::string&user_tel,Json::Value &res);
bool Db_Cancel_Ticket(const std::string &user_tel,const std::string &yd_id);
void Send_full();
void Mysql_Begin();
void Mysql_RollBack();
void Mysql_Commit();
};
//连接池中的每一个与数据库进行连接的客户端入口结构体
struct Mysql_cli{
MYSQL* mysql_con;
bool status;
};
class Connect_Pool{
private:
bool status;
int MAX_USER;
Mysql_config* Sql_config;
public:
Mysql_cli* C_P_array;
int user;
pthread_mutex_t mutex;
Connect_Pool(Mysql_config* Sql_config);
~Connect_Pool();
bool Run();
void Reset();
};
class Tcp_Service{
private:
const Service_Config* config;
Thread_Pool* pool;
int lis_fd = START;
int ep_fd = START;
bool Status;
struct epoll_event ready_events[MAXLINE]{0};
public:
Tcp_Service(const Service_Config* config);
~Tcp_Service();
void Init(const Service_Config* config);
};
文件作用
集中声明服务端所有类型:配置类、Socket 体系、线程池、MySQL 客户端、连接池、Tcp_Service 主控。
关键类型一览
| 类型 | 行号 | 职责 |
|---|---|---|
Service_Config |
L34-L43 | 读 service.conf,存 IP/Port/Lismax |
Socket(抽象基类) |
L48-L56 | 持有 fd + epfd,纯虚 Do_client() |
AccSocket |
L58-L66 | 处理 listen fd 上的 accept 事件 |
RecvSocket |
L68-L76 | 处理 client fd 上的 recv 事件 |
task_t |
L78-L80 | 任务结构体(包 Socket* 指针) |
Thread_Pool |
L82-L105 | 任务队列 + 5 个 worker 线程 |
Mysql_config |
L110-L118 | 读 mysql.conf,存 db_ip/port/name |
Mysql_Client |
L120-L145 | 业务层,封装 SQL 操作 |
Mysql_cli |
L148-L151 | 连接池中单个 slot(MYSQL* + status) |
Connect_Pool |
L155-L172 | 5 个 Mysql_cli + mutex |
Tcp_Service |
L174-L191 | 主控:listen + epoll 循环 |
设计亮点
- 前置声明
class Connect_Pool;(L31)让g_pool全局指针可用,定义放在后面 Socket抽象基类 +Do_client()纯虚函数,多态分发 accept/recv 任务Mysql_Client::IsConnected()(L130)提供连接状态查询,避免外部访问私有成员
不足
- 10 个类型挤一个文件,违反单一职责
Mysql_cli是 struct 但成员公开,封装不严
4.2 <service.cpp> --- 服务端实现
完整代码
cpp
#include "service.h"
#include <errno.h>
#include <fcntl.h>
#include <iostream>
#include <jsoncpp/json/reader.h>
#include <jsoncpp/json/value.h>
#include <mysql/mysql.h>
#include <netinet/in.h>
#include <pthread.h>
#include <string>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <unistd.h>
using namespace std;
Mysql_config file;
Connect_Pool* g_pool = nullptr;
enum OP_TYPE {
Login = 1,
Register,
CheckApoint,
Appointment,
AppoinMessage,
CancelAppoin,
Exit
};
void Service_Config::ReadConfig(const char *config_path) {
if (config_path == nullptr) {
cout << "config_path is nullptr!" << endl;
return;
}
FILE *fp = fopen(config_path, "r");
if (fp == nullptr) {
perror("fopen config file failed");
cout << "fopen config file failed! errno=" << errno << endl;
return;
}
char line[MAXLINE]{0};
int num = 0;
while (fgets(line, MAXLINE - 2, fp) != nullptr) {
num++;
if (line[0] == '#' || line[0] == '\n') {
continue;
}
char *Before = strtok(line, "=");
char *After = strtok(nullptr, "=");
if (Before == nullptr || After == nullptr) {
continue;
}
After[strlen(After) - 1] = '\0';
if (strcmp("ip", Before) == 0) {
IP = After; // 禁止共享内存,line是一次性使用,After会被覆盖
} else if (strcmp("port", Before) == 0) {
Port = atoi(After);
} else if (strcmp("lismax", Before) == 0) {
Lismax = atoi(After);
} else if (strcmp("service_conf_path", Before) == 0) {
config_path = After;
} else {
cout << "unknown config item: " << Before << " at line " << num << endl;
}
memset(line, 0, sizeof(line));
}
fclose(fp);
}
void Service_Config::PrintConfig() const {
cout << "IP: " << IP << endl;
cout << "Port: " << Port << endl;
cout << "Lismax: " << Lismax << endl;
cout << "config_path: " << config_path << endl;
}
Tcp_Service::Tcp_Service(const Service_Config *config) { Init(config); }
Tcp_Service::~Tcp_Service() {
cout << "~Tcp_Service()" << endl;
close(lis_fd);
close(ep_fd);
if (pool != nullptr)
delete pool;
}
Socket::Socket(int fd, int epfd) : fd(fd), epfd(epfd) {}
Socket::~Socket() {}
AccSocket::AccSocket(int fd, int epfd) : Socket(fd, epfd) {}
unsigned int AccSocket::count = 0;
AccSocket::~AccSocket() {}
void AccSocket::Do_client() {
struct sockaddr_in client_addr;
socklen_t client_addr_len = sizeof(client_addr);
memset(&client_addr, 0, sizeof(client_addr));
// lis_fd对accept的影响,在默认情况下,lis_fd在队列为空时,会默认让accept在获取这个空队列时阻塞,线程会直接阻塞卡死在这
// 所以我们要用非阻塞模式的accept,当队列为空时,accept会返回EAGAIN或EWOULDBLOCK,而不是阻塞线程
int conn_fd = accept(fd, (struct sockaddr *)&client_addr, &client_addr_len);
if (conn_fd < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
// listen 队列空了,别的 worker 已经把连接取走了,正常退出
delete this;
return;
}
perror("accept failed");
cout << "accept failed! errno=" << errno << endl;
delete this;
return;
}
cout << "client:" << conn_fd << " connect success!" << endl;
cout << "Service accept client numb now: " << ++count << endl;
cout << "-------------------------------------" << endl;
struct epoll_event ev_client_accpet;
ev_client_accpet.events = EPOLLIN | EPOLLONESHOT;
auto *recvSock = new RecvSocket(conn_fd, epfd);
ev_client_accpet.data.ptr = recvSock;
if (epoll_ctl(epfd, EPOLL_CTL_ADD, conn_fd, &ev_client_accpet) == -1) {
perror("epoll_ctl add client fd failed");
cout << "epoll_client_ctl add false! errno=" << errno << endl;
close(conn_fd); // 关闭连接,不受监控的不要
delete recvSock; // 删除对象
}
delete this;
}
RecvSocket::RecvSocket(int fd, int pdfd) : Socket(fd, pdfd) {}
RecvSocket::~RecvSocket() { close(fd); }
void RecvSocket::send_Ok() {
Json::Value value;
string a = o;
value["status"] = a;
send(fd, value.toStyledString().c_str(),
strlen(value.toStyledString().c_str()), 0);
}
void RecvSocket::send_false() {
Json::Value value;
string a = "false";
value["status"] = a;
send(fd, value.toStyledString().c_str(),
strlen(value.toStyledString().c_str()), 0);
}
void RecvSocket::Do_client() {
char buff[MAXLINE]{0};
ssize_t recv_num = recv(fd, buff, sizeof(buff) - 1, 0);
bool tag = false;
if (recv_num < 0) {
perror("recv failed");
cout << "[fd=" << fd << "] recv 出错 errno=" << errno << endl;
tag = true;
} else if (recv_num == 0) {
cout << "[fd=" << fd << "] 客户端正常关闭连接" << endl;
tag = true;
} else {
buff[recv_num] = '\0';
cout << "[fd=" << fd << "] 收到 " << recv_num << " 字节" << endl;
cout << "---------------------------------------------" << endl;
Json::Value val;
Json::Reader read;
if (!read.parse(buff, val)) {
cout << "[fd=" << fd << "] Json 解析失败! 收到内容: " << buff << endl;
tag = true;
} else {
Mysql_Client sql(&file,fd);
if (sql.IsConnected()) {
int i = val["type"].asInt();
if (i == Login) {
string user_tel = val["user_tel"].asString();
string passwd = val["passwd"].asString();
string user_name = val["user_name"].asString();
if (sql.Db_User_Login(user_tel, user_name, passwd)) {
send_Ok();
} else {
send_false();
}
} else if (i == Register) {
string tel = val["user_tel"].asString();
string name = val["user_name"].asString();
string passwd = val["passwd"].asString();
if (sql.Db_User_Register(tel, name, passwd)) {
send_Ok();
} else {
send_false();
}
} else if (i == CheckApoint) {
Json::Value Reader;
if (!sql.Db_Show_Ticket(Reader)) {
send_false();
} else
send(fd, Reader.toStyledString().c_str(),
strlen(Reader.toStyledString().c_str()), 0);
} else if (i == Appointment) {
string user_tel = val["user_tel"].asString();
string ticket_id = val["ticket_id"].asString();
if (!sql.Db_Yd_Ticket(user_tel, ticket_id)) {
send_false();
} else {
send_Ok();
}
} else if (i == AppoinMessage) {
string user_tel = val["user_tel"].asString();
Json::Value res;
if (!sql.Db_Show_My_Ticket(user_tel, res)) {
send_false();
} else {
send(fd, res.toStyledString().c_str(),
strlen(res.toStyledString().c_str()), 0);
}
} else if (i == CancelAppoin) {
string user_tel = val["user_tel"].asString();
string yd_id = val["yd_id"].asString();
if (!sql.Db_Cancel_Ticket(user_tel, yd_id)) {
send_false();
} else {
send_Ok();
}
} else {
tag = true;
}
}
}
}
if (tag) {
epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
delete this;
} else {
struct epoll_event ev;
ev.events = EPOLLIN | EPOLLONESHOT;
ev.data.ptr = this;
epoll_ctl(epfd, EPOLL_CTL_MOD, fd, &ev);
}
}
Thread_Pool::Thread_Pool() {
if (!Init_Thread_pool()) {
exit(1);
}
Create_Thread();
}
Thread_Pool::~Thread_Pool() {
pthread_mutex_lock(&mutex);
status = false;
pthread_cond_broadcast(&cond); // 唤醒所有在 cond_wait 的线程
pthread_mutex_unlock(&mutex);
for (int i = 0; i < thread_num; i++) {
pthread_join(Thread_array[i], NULL);
}
free(dequeue);
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);
}
bool Thread_Pool::Init_Thread_pool() {
dequeue = (task_t *)malloc(sizeof(task_t) * d_size);
if (dequeue == nullptr) {
cout << "dequeue malloc failure" << endl;
return false;
}
dequeue_size = d_size;
front = 0;
rear = front;
task_Num = 0;
thread_Num = thread_num;
pthread_cond_init(&cond, NULL);
pthread_mutex_init(&mutex, NULL);
status = true;
cout << "ThreadPool init success!" << endl;
cout << "---------------------------------------" << endl;
return true;
}
void Thread_Pool::add_Task(Socket *csocket) {
pthread_mutex_lock(&mutex);
if (task_Num >= dequeue_size) {
delete csocket;
cout << "dequeue_size is full!add_Task false!" << endl;
pthread_mutex_unlock(&mutex);
return;
}
dequeue[rear].task = csocket;
rear = (rear + 1) % dequeue_size;
task_Num++;
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);
}
void *General_Thread(void *arg) {
Thread_Pool *poo = (Thread_Pool *)arg;
poo->Work();
return NULL;
}
void Thread_Pool::Create_Thread() {
for (int i = 0; i < thread_num; i++) {
pthread_create(&Thread_array[i], NULL, General_Thread, this);
}
}
void Thread_Pool::Work() {
while (true) {
pthread_mutex_lock(&mutex);
while (task_Num == 0 && status == true) {
pthread_cond_wait(&cond, &mutex);
// 这是条件休眠,进入到条件变量的休眠队列,进入条件变量的休眠队列是会自动释放锁,这与sleep是不相同的。
} // 不能用if,要用while,以防虚假唤醒。
if (status == false) {
cout << "ThreadPool not run....." << endl;
break;
} // 处理线程池未启用的状态。
Socket *tmp = dequeue[front].task;
if (tmp == nullptr) {
cout << "tmp is nullptr" << endl;
break;
}
front = (front + 1) % dequeue_size;
task_Num--;
// cout<<"Thread id: "<<pthread_self()<<endl;
pthread_mutex_unlock(&mutex);
tmp->Do_client();
}
// 发生意外情况也要及时释放锁
pthread_mutex_unlock(&mutex);
}
void Mysql_config::Init_Mysql_config(const char *Sql_config_path) {
if (Sql_config_path == nullptr) {
cout << "config_path is nullptr!" << endl;
return;
}
FILE *fp = fopen(Sql_config_path, "r");
if (fp == nullptr) {
perror("fopen config file failed");
cout << "fopen config file failed! errno=" << errno << endl;
return;
}
char line[MAXLINE]{0};
int num = 0;
while (fgets(line, MAXLINE - 2, fp) != nullptr) {
num++;
if (line[0] == '#' || line[0] == '\n') {
continue;
}
char *Before = strtok(line, "=");
char *After = strtok(nullptr, "=");
if (Before == nullptr || After == nullptr) {
continue;
}
After[strlen(After) - 1] = '\0';
if (strcmp("dp_ip", Before) == 0) {
db_ip = After; // 禁止共享内存,line是一次性使用,After会被覆盖
} else if (strcmp("dp_port", Before) == 0) {
db_port = atoi(After);
} else if (strcmp("dp_name", Before) == 0) {
db_name = After;
} else if (strcmp("dp_mysql_path", Before) == 0) {
Sql_config_path = After;
} else {
cout << "unknown config item: " << Before << " at line " << num << endl;
}
memset(line, 0, sizeof(line));
}
fclose(fp);
print_Mysql_config();
}
void Mysql_config::print_Mysql_config() const {
cout << "db_ip: " << db_ip << endl;
cout << "db_port: " << db_port << endl;
cout << "db_name: " << db_name << endl;
cout << "config_path: " << Sql_config_path << endl;
}
void Mysql_Client::Send_full(){
string a = "MYSQL CLIENT BUSSY";
Json::Value value;
value["status"] = a;
send(who, value.toStyledString().c_str(),
strlen(value.toStyledString().c_str()), 0);
}
Mysql_Client::
Mysql_Client(struct Mysql_config *Sql_config,int who):who(who) {
connected = false;
mysql_con = nullptr;
if (Sql_config == nullptr) {
cout << "Mysql_config_path nullptr!" << endl;
Send_full();
return; // 不 exit, 让客户端收到 busy 后自己重试
}
file = (struct Mysql_config *)Sql_config;
if (!Connect_Mysql_Server()) {
cout << "---------------------------------------------" << endl;
cout << "connect mysql false (pool busy)" << endl;
cout << "---------------------------------------------" << endl;
Send_full(); // 给客户端发 "MYSQL CLIENT BUSSY"
return; // 不再继续, 后续 Db_* 也不会被调用
}
connected = true;
cout << "connect mysql succeed" << endl;
cout << "---------------------------------------------" << endl;
}
void Mysql_Client::Mysql_Begin() {
if (!connected) return;
if (mysql_query(mysql_con, "begin") != 0) {
cout << "事物启动失败!" << endl;
}
}
void Mysql_Client::Mysql_RollBack() {
if (!connected) return;
if (mysql_query(mysql_con, "rollback") != 0) {
cout << "事物回滚失败!" << endl;
}
}
void Mysql_Client::Mysql_Commit() {
if (!connected) return;
if (mysql_query(mysql_con, "commit") != 0) {
cout << "事物提交失败!" << endl;
}
}
bool Mysql_Client::Connect_Mysql_Server() {
pthread_mutex_lock(&g_pool->mutex);
int i=0;
while(i<thread_num){
if(g_pool->C_P_array[i].status==true)
{
mysql_con = g_pool->C_P_array[i].mysql_con;
g_pool->C_P_array[i].status=false;
break;
}
i++;
}
if(i>=thread_num){
pthread_mutex_unlock(&g_pool->mutex);
return false;
}
else{
g_pool->user++;
pthread_mutex_unlock(&g_pool->mutex);
return true;
}
}
bool Mysql_Client::Db_User_Register(const std::string &user_tel,
const std::string &user_name,
const std::string &user_passwd) {
if (!connected) return false;
string sql = string("insert into user_info values(0,'") + user_tel +
string("','") + user_name + string("','") + user_passwd +
string("',1,CURDATE())");
if (mysql_query(mysql_con, sql.c_str()) != 0) {
return false;
}
return true;
}
bool Mysql_Client::Db_User_Login(const std::string &tel, std::string &name,
const std::string &passwd) {
if (!connected) return false;
string sql = string("select Name,Passwd from user_info where Tel='") + tel +
string("'");
if (mysql_query(mysql_con, sql.c_str()) != 0) {
return false;
}
MYSQL_RES *r = mysql_store_result(mysql_con);
if (r == nullptr) {
return false;
}
int num = mysql_num_rows(r);
if (num == 0) {
return false;
}
MYSQL_ROW row = mysql_fetch_row(r);
if (row == nullptr) {
return false;
}
string username = row[0];
string userpasswd = row[1];
mysql_free_result(r);
if (userpasswd != passwd || name != username) {
return false;
}
return true;
}
bool Mysql_Client::Db_Show_Ticket(Json::Value &res) {
if (!connected) return false;
string sql = "select * from ticket_table where status=1";
if (mysql_query(mysql_con, sql.c_str()) != 0) {
return false;
}
MYSQL_RES *r = mysql_store_result(mysql_con);
if (r == nullptr) {
return false;
}
int num = mysql_num_rows(r);
if (num == 0) {
return false;
}
res["status"] = o;
res["num"] = num;
for (int i = 0; i < num; i++) {
MYSQL_ROW row = mysql_fetch_row(r);
Json::Value val;
val["ticket_id"] = atoi(row[0]);
val["ticket_name"] = row[1];
val["ticket_max"] = row[2];
val["ticket_count"] = row[3];
val["day_time"] = row[4];
res["ticket_arr"].append(val); // Json对象也可以添加进去
}
mysql_free_result(r);
return true;
}
bool Mysql_Client::Db_Yd_Ticket(std::string usertel, std::string tk_id) {
if (!connected) return false;
string sql_max_count =
string("select tk_max,count from ticket_table where Tk_id=") + tk_id;
if (mysql_query(mysql_con, sql_max_count.c_str()) != 0) {
cout << "[预约] 查询票信息失败: " << mysql_error(mysql_con)
<< " | SQL: " << sql_max_count << endl;
return false;
}
MYSQL_RES *r = mysql_store_result(mysql_con);
if (r == NULL) {
cout << "[预约] 取结果集失败: " << mysql_error(mysql_con) << endl;
return false;
}
int num = mysql_num_rows(r);
if (num == 0) {
cout << "[预约] 未找到 Tk_id=" << tk_id << " 的票记录" << endl;
mysql_free_result(r);
return false;
}
MYSQL_ROW row = mysql_fetch_row(r);
int max = atoi(row[0]);
int count = atoi(row[1]);
mysql_free_result(r);
if (count >= max) {
cout << "[预约] 票数不足 max=" << max << " count=" << count << endl;
return false;
}
count++;
Mysql_Begin();
// 一旦开启事物,这两条sql语句都成功执行的情况下,才会去进行提交事物的操作,
// 否则一旦有其中一个未成功,就算其中一个成功的操作也会撤销,二者立刻回滚,所有改动立即撤销,
string sql_setcount = string("update ticket_table set count=") +
to_string(count) + string(" where Tk_id=") + tk_id;
if (mysql_query(mysql_con, sql_setcount.c_str()) != 0) {
cout << "[预约] 更新票数失败: " << mysql_error(mysql_con)
<< " | SQL: " << sql_setcount << endl;
Mysql_RollBack();
return false;
}
string sql_yd = string("insert into yd_table values(0,'") + usertel +
string("','") + tk_id + string("',now())");
if (mysql_query(mysql_con, sql_yd.c_str()) != 0) {
cout << "[预约] 写入预约表失败: " << mysql_error(mysql_con)
<< " | SQL: " << sql_yd << endl;
Mysql_RollBack();
return false;
}
Mysql_Commit();
cout << "[预约] 成功: user_tel=" << usertel << " Tk_id=" << tk_id
<< " 剩余=" << (max - count) << endl;
return true;
}
bool Mysql_Client::Db_Show_My_Ticket(const std::string &user_tel,
Json::Value &res) {
if (!connected) return false;
string sql =
string("select y.yd_id, t.tk_name, t.tk_max, t.`count`, t.DateTime ") +
string("from yd_table y, ticket_table t ") +
string("where y.user_tel='") + user_tel + string("' and y.tk_id=t.Tk_id");
if (mysql_query(mysql_con, sql.c_str()) != 0) {
cout << mysql_error(mysql_con) << endl;
return false;
}
MYSQL_RES *r = mysql_store_result(mysql_con);
if (r == nullptr) {
res["status"] = o;
res["num"] = 0;
return true;
}
int num = mysql_num_rows(r);
res["status"] = o;
res["num"] = num;
for (int i = 0; i < num; i++) {
MYSQL_ROW row = mysql_fetch_row(r);
Json::Value val;
val["yd_id"] = row[0];
val["ticket_name"] = row[1];
val["ticket_max"] = row[2];
val["ticket_count"] = row[3];
val["day_time"] = row[4];
res["my_ticket_arr"].append(val);
}
mysql_free_result(r);
return true;
}
bool Mysql_Client::Db_Cancel_Ticket(const std::string &user_tel,
const std::string &yd_id) {
if (!connected) return false;
string sql_select = string("select tk_id from yd_table where yd_id=") +
yd_id + string(" and user_tel='") + user_tel +
string("'");
// 因为我们删除的时候是删除我们自己的预约表的最前面的那个编号,而不是票的票号,所以我们要额外的去存储这个票的票号,对这个票的数据进行减一的操作。
if (mysql_query(mysql_con, sql_select.c_str()) != 0) {
cout << "查询预约记录失败: " << mysql_error(mysql_con) << endl;
return false;
}
MYSQL_RES *r = mysql_store_result(mysql_con);
if (r == nullptr) {
return false;
}
int num = mysql_num_rows(r);
if (num == 0) {
mysql_free_result(r);
return false; // 预约记录不存在,无法取消
}
MYSQL_ROW row = mysql_fetch_row(r);
string tk_id = row[0];
mysql_free_result(r);
Mysql_Begin();
string sql_del = string("delete from yd_table where yd_id=") + yd_id;
if (mysql_query(mysql_con, sql_del.c_str()) != 0) {
cout << mysql_error(mysql_con) << endl;
Mysql_RollBack();
return false;
}
string sql_update =
string("update ticket_table set `count`=`count`-1 where Tk_id=") + tk_id;
if (mysql_query(mysql_con, sql_update.c_str()) != 0) {
cout << mysql_error(mysql_con) << endl;
Mysql_RollBack();
return false;
}
Mysql_Commit();
string sql_reset = "ALTER TABLE yd_table AUTO_INCREMENT = 1";
if (mysql_query(mysql_con, sql_reset.c_str()) != 0) {
cout << mysql_error(mysql_con) << endl;
}
return true;
}
Mysql_Client::~Mysql_Client() {
pthread_mutex_lock(&g_pool->mutex);
for(int i=0;i<thread_num;i++){
if(mysql_con==g_pool->C_P_array[i].mysql_con){
g_pool->C_P_array[i].status=true;
g_pool->user--;
}
}
pthread_mutex_unlock(&g_pool->mutex);
}
Connect_Pool::Connect_Pool(Mysql_config *Sql_config) {
if (Sql_config == nullptr) {
cout << "Connect Sqlconfig == nullptr!" << endl;
return;
}
user = 0;
status = true;
MAX_USER = thread_num;
this->Sql_config = Sql_config;
C_P_array = new Mysql_cli[thread_num];
Reset();
pthread_mutex_init(&mutex,NULL);
if(!Run()){
cout<<"Connect_Pool Run failed!"<<endl;
Reset();
exit(1);
}
cout<<"-------------------------"<<endl;
cout<<"Connect_Pool Run success!"<<endl;
cout<<"-------------------------"<<endl;
}
bool Connect_Pool::Run() {
if (status == false) {
return false;
}
for (int i = 0; i < thread_num; i++) {
MYSQL *mysql = mysql_init(C_P_array[i].mysql_con);
if (mysql == nullptr) {
return false;
}
mysql = mysql_real_connect(mysql, Sql_config->db_ip.c_str(), "root",
"111111", Sql_config->db_name.c_str(),
Sql_config->db_port, NULL, 0);
if (mysql == nullptr) {
return false;
}
C_P_array[i].mysql_con = mysql;
C_P_array[i].status = true;
}
return true;
}
void Connect_Pool::Reset(){
for(int i=0;i<thread_num;i++){
if(C_P_array[i].mysql_con!=nullptr){
mysql_close( C_P_array[i].mysql_con);
}
C_P_array[i].mysql_con= nullptr;
C_P_array[i].status = false;
}
}
Connect_Pool::~Connect_Pool(){
for(int i = 0;i<thread_num;i++){
mysql_close(C_P_array[i].mysql_con);
}
delete C_P_array;
pthread_mutex_destroy(&mutex);
}
void Tcp_Service::Init(const Service_Config *config) {
if (config == nullptr) {
cout << "config is nullptr!" << endl;
return;
}
this->config = config;
Status = true;
pool = new Thread_Pool;
Mysql_config file;
file.Init_Mysql_config(Sql_Conf_path);
lis_fd = socket(AF_INET, SOCK_STREAM, 0);
if (lis_fd == START) {
perror("socket failed");
cout << "socket failed! errno=" << errno << endl;
exit(1);
}
// listen socket 设为非阻塞,accept 队列空时返回 EAGAIN 而不是阻塞
int flags = fcntl(lis_fd, F_GETFL, 0);
fcntl(lis_fd, F_SETFL, flags | O_NONBLOCK);
// 因为是lis_fd为空导致的阻塞,所以我们这个将模式设为非阻塞,在此之后,accept会直接返回EAGAIN或EWOULDBLOCK,而不是阻塞线程
// 不会阻塞
int opt = 1;
setsockopt(lis_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = inet_addr(config->IP.c_str());
server_addr.sin_port = htons(config->Port);
config->PrintConfig();
if (bind(lis_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
perror("bind failed");
cout << "bind failed! errno=" << errno << endl;
exit(1);
}
if (listen(lis_fd, config->Lismax) < 0) {
perror("listen failed");
cout << "listen init error! errno=" << errno << endl;
exit(1);
}
int epfd =
epoll_create1(EPOLL_CLOEXEC); // 防止fork版本公用一个文件描述符的情况
if (-1 == epfd) {
perror("epoll_create1 failed");
cout << "create epoll error! errno=" << errno << endl;
close(lis_fd);
exit(1);
}
ep_fd = epfd;
cout << "epoll success!" << endl;
struct epoll_event ev_listen;
ev_listen.events = EPOLLIN;
ev_listen.data.fd = lis_fd;
if (epoll_ctl(epfd, EPOLL_CTL_ADD, lis_fd, &ev_listen) == -1) {
perror("epoll_ctl add listen fd failed");
cout << "epoll_ctl add false! errno=" << errno << endl;
close(lis_fd);
exit(1);
}
cout << "Tcp service success!" << endl;
cout << "------------------------------------------------------" << endl;
while (Status) {
int ready_count = epoll_wait(epfd, ready_events, MAXLINE, -1);
//- 内核 :负责把发生事件的 fd 放入就绪双链表(这是中断/回调驱动的,随时发生)
// epoll_wait :负责从就绪双链表取出事件,拷贝给用户态的 ready_events
if (ready_count == -1) {
perror("epoll_wait failed");
cout << "epoll_wait error! errno=" << errno << endl;
close(lis_fd);
exit(1);
}
for (int i = 0; i < ready_count; i++) {
int tmfd = ready_events[i].data.fd;
if (tmfd == lis_fd) {
auto csocket = new AccSocket(lis_fd, epfd);
pool->add_Task(csocket);
} else {
auto csocket = ready_events[i].data.ptr;
if (csocket == nullptr) {
cout << "csocket is nullptr" << endl;
continue;
}
pool->add_Task((Socket *)csocket);
}
}
}
}
int main() {
file.Init_Mysql_config(Sql_Conf_path);
file.print_Mysql_config();
cout << "-----------------------------------------" << endl;
g_pool = new Connect_Pool(&file);
Service_Config a;
a.ReadConfig(a.config_path);
cout << "-----------------------------------------" << endl;
Tcp_Service tcp_service(&a);
delete g_pool;
return 0;
}
文件作用
实现 service.h 里所有类的方法 + main 函数(约 935 行)。
核心流程
main()
├─ file.Init_Mysql_config() // 读 mysql.conf
├─ g_pool = new Connect_Pool(&file) // 建连接池(5 个连接)
├─ a.ReadConfig() // 读 service.conf
└─ Tcp_Service tcp_service(&a) // 进入主循环
├─ new Thread_Pool // 起 5 个 worker
├─ listen + bind + socket // 起 TCP 服务
└─ while (Status) { // epoll 主循环
epoll_wait()
├─ if listen fd → new AccSocket → add_Task
└─ else → 取 RecvSocket* → add_Task
}
关键方法详解
Tcp_Service::Init (L815-L917)
主控函数:建线程池 → 建 listen fd(设非阻塞 + SO_REUSEADDR)→ bind → listen → epoll_create1 → epoll_ctl ADD listen fd → while(Status) 循环 epoll_wait。
Thread_Pool::Work (L338-L366)
worker 线程主循环:pthread_mutex_lock → while(task_Num==0) cond_wait → 取出 dequeue[front].task → unlock → tmp->Do_client() 多态分发。
RecvSocket::Do_client (L164-L260)
- recv 数据 → JSON 解析 → 构造
Mysql_Client sql(&file, fd) - 检查
sql.IsConnected():- false(池满)→ 跳过 Db_* 调用,构造里已
Send_full()发了 BUSSY - true → 按
val["type"]走 6 个业务分支
- false(池满)→ 跳过 Db_* 调用,构造里已
- 结束后
epoll_ctl MOD重新监听(EPOLLONESHOT)
Mysql_Client::Connect_Mysql_Server (L474-L499)
加锁遍历 C_P_array,找到 status==true 的 slot → mysql_con = C_P_array[i].mysql_con → status=false → user++ → 返回 true。借不到返回 false。
Mysql_Client::~Mysql_Client (L740-L749)
加锁遍历 C_P_array,找到 mysql_con 相等的 slot → status=true → user-- 归还。
Connect_Pool::Run (L776-L798)
循环 5 次:mysql_init → mysql_real_connect → 存进 C_P_array[i].mysql_con + status=true。
Connect_Pool::Reset (L801-L809)
循环 5 次:若 mysql_con != nullptr 则 mysql_close,然后置 nullptr + status=false。
业务方法
Db_User_Register(L501) --- 插入 user_infoDb_User_Login(L517) --- select Name,Passwd 比对Db_Show_Ticket(L552) --- select * 拼成 JSONDb_Yd_Ticket(L585) --- 事务:查 max/count → 更新 count → 插 yd_tableDb_Show_My_Ticket(L647) --- JOIN yd_table + ticket_tableDb_Cancel_Ticket(L684) --- 事务:查 tk_id → 删 yd → count-1
设计亮点
Mysql_Client::IsConnected()让 Do_client 可以无侵入处理"池满"Send_full()直接发 BUSSY 给客户端 fd,避免在 Do_client 多发一次 false- 事务在
Db_Yd_Ticket/Db_Cancel_Ticket中正确 begin/commit/rollback - listen fd 设非阻塞 + accept 返回 EAGAIN 时不报错静默退出(多 worker 竞争保护)
不足
Mysql_Client::mysql_con之前是野指针(已加mysql_con = nullptr;初始化修好)day-timevsday_time字段名不一致(已修)Connect_Pool::Run失败时局部 mysql 指针丢失(OS 兜底)- SQL 全字符串拼接,存在注入风险
4.3 <client.h> --- 客户端类声明
完整代码
cpp
#include <stdlib.h>
#include <string>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <jsoncpp/json/json.h>
#include <mysql/mysql.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <pthread.h>
#include <unistd.h>
#define IPADRESS "127.0.0.1"
#define PORTADRESS 8888
#define START -1
#define Length 1024
#define k "ok"
#define BUSY "MYSQL CLIENT BUSSY" // 服务器连接池满时返回的标记
class Client{
private:
short Port;
int conn_fd;
int User_Login_way;
bool Running;
bool Dl_Status;
std::string IP;
std::string user_name;
std::string user_tel;
public:
Client();
~Client();
void Client_Init();
void Print_Config();
void Run();
void Show_Menu();
void User_Register();
void User_login();
void Show_Ticket();
void Yd_Ticket();
void Show_My_Ticket();
void Qx_Ticket();
};
文件作用
声明 Client 类,定义客户端常量(IP/PORT/Buff 大小)。
关键成员
cpp
#define IPADRESS "127.0.0.1"
#define PORTADRESS 8888
#define Length 1024
#define k "ok"
#define BUSY "MYSQL CLIENT BUSSY" // 服务器连接池满时返回的标记
Client 类
| 成员 | 作用 |
|---|---|
conn_fd |
TCP 连接 fd |
Dl_Status |
登录状态 |
Running |
主循环开关 |
user_name / user_tel |
当前用户信息 |
方法
Client_Init()--- socket + connectShow_Menu()--- 显示菜单(登录前后不同)User_Register / User_login / Show_Ticket / Yd_Ticket / Show_My_Ticket / Qx_Ticket--- 6 个业务Run()--- 主循环 switch 分发
4.4 <client.cpp> --- 客户端实现
完整代码
cpp
#include "client.h"
#include <jsoncpp/json/value.h>
#include <ostream>
#include <sys/socket.h>
#include <iostream>
using namespace std;
enum OP_TYPE{
Login = 1,
Register,
CheckApoint,
Appointment,
AppoinMessage,
CancelAppoin,
Exit
};
Client::Client()
{
IP=IPADRESS;
Port = PORTADRESS;
conn_fd = START;
Dl_Status = false;
Running = true;
Client_Init();
}
Client::~Client()
{
if(conn_fd != START){
close(conn_fd);
}
cout<<"Client close"<<endl;
}
void Client::Client_Init(){
int sock_fd = socket(AF_INET,SOCK_STREAM,0);
if(sock_fd<=0){
cout<<"Client socket Error!"<<endl;
exit(1);
}
conn_fd = sock_fd;
Print_Config();
struct sockaddr_in Client_sock_ad;
memset(&Client_sock_ad,0,sizeof(Client_sock_ad));
Client_sock_ad.sin_family = AF_INET;
Client_sock_ad.sin_addr.s_addr=inet_addr(IPADRESS);
Client_sock_ad.sin_port = htons(PORTADRESS);
if(connect(sock_fd,(const struct sockaddr* )&Client_sock_ad,sizeof(Client_sock_ad))==START){
cout<<"connect failed!"<<endl;
close(sock_fd);
exit(1);
}
cout<<"connect succed!"<<endl;
cout<<" client start begin "<<endl;
cout<<"------------------------------------------------------"<<endl;
Run();
}
void Client::Print_Config(){
cout<<"IP: "<<IP<<endl;
cout<<"Port: "<<Port<<endl;
cout<<"conn_fd: "<<conn_fd<<endl;
cout<<"this: "<<this<<endl;
}
void Client::Show_Menu(){
if(!Dl_Status){
cout <<"---用户名:游客---------状态:未登录-----"<<endl;
cout << "1 登陆 2 注册 3 退出" << endl;
cout << "---------------------------------" << endl;
cout << "请输入选项编号:" << endl;
}
else{
cout << "---用户名:" << user_name << "--------状态:已登陆------" << endl;
cout << "1 查看预约 2 预定 " << endl;
cout << "3 查看我的预约 4 取消预定 " << endl;
cout << " 5 退出" << endl;
cout << "---------------------------------" << endl;
cout << "请输入选项编号:" << endl;
cout << "---------------------------------" << endl;
}
}
void Client::User_Register(){
cout<<"请输入手机号码:"<<endl;
cin>>user_tel;
cout<<"请输入用户名:"<<endl;
cin>>user_name;
cout<<"请输入密码"<<endl;
string passwd;
cin>>passwd;
Json::Value mess;
mess["type"] = Register;
mess["user_tel"] = user_tel;
mess["user_name"] = user_name;
mess["passwd"] = passwd;
send(conn_fd,mess.toStyledString().c_str(),strlen(mess.toStyledString().c_str()),0);
char buff_status[Length] = {0};
int n = recv(conn_fd,buff_status,Length-1,0);
if(n<0){
cout<<"ser err"<<endl;
return;
}
Json::Value Acmess;
Json::Reader Read;
if(!Read.parse(buff_status,Acmess)){
cout<<"Json Read error!"<<endl;
return;
}
string st = Acmess["status"].asString();
if(st == BUSY){
cout<<"服务器繁忙,请稍后重试"<<endl;
return ;
}
if(st.compare(k)!=0){
cout<<"Register false!"<<endl;
return ;
}
cout<<"Register success!"<<endl;
Dl_Status = true;
}
void Client::User_login(){
cout<<"请输入手机号码"<<endl;
cin>>user_tel;
cout<<"请输入用户名"<<endl;
cin>>user_name;
cout<<"请输入密码"<<endl;
string passwd;
cin>>passwd;
Json::Value val;
val["type"] = Login;
val["user_tel"] = user_tel;
val["passwd"] = passwd;
val["user_name"] = user_name;
send(conn_fd,val.toStyledString().c_str(),strlen(val.toStyledString().c_str()),0);
char buff_status[Length] = {0};
int n = recv(conn_fd,buff_status,Length-1,0);
if(n<0){
cout<<"ser err"<<endl;
return;
}
Json::Value Acmess;
Json::Reader Read;
if(!Read.parse(buff_status,Acmess)){
cout<<"Json Read error!"<<endl;
return;
}
string st = Acmess["status"].asString();
if(st == BUSY){
cout<<"服务器繁忙,请稍后重试"<<endl;
return ;
}
if(st.compare(k)!=0){
cout<<"Login false!"<<endl;
return ;
}
cout<<"Login success!"<<endl;
Dl_Status = true;
}
void Client::Show_Ticket(){
Json::Value val;
val["type"] = CheckApoint;
send(conn_fd,val.toStyledString().c_str(),strlen(val.toStyledString().c_str()),0);
char buff[Length]={0};
int n = recv(conn_fd,buff,Length-1,0);
if(n<=0){
cout<<"ser close"<<endl;
return;
}
Json::Value res_val;
Json::Reader Read;
if(!Read.parse(buff,res_val)){
cout<<"Json 解析失败"<<endl;
return;
}
string s = res_val["status"].asString();
if(s == BUSY){
cout<<"服务器繁忙,请稍后重试"<<endl;
return;
}
if(s.compare(k)!=0){
cout<<"查看预约信息失败"<<endl;
return;
}
int num = res_val["num"].asInt();
if(num<0){
cout<<"暂时没有可以预约的信息"<<endl;
return ;
}
cout<<"|----------|--------------|--------|--------|------------|"<<endl;
cout<<"| 序号 | 名称 | 总票数 | 已预定 | 时间 |"<<endl;
cout<<"|----------|--------------|--------|--------|------------|"<<endl;
for(int i=0;i<num;i++){
printf("| %8s | %12s | %6s | %6s | %10s |\n",
res_val["ticket_arr"][i]["ticket_id"].asString().c_str(),
res_val["ticket_arr"][i]["ticket_name"].asString().c_str(),
res_val["ticket_arr"][i]["ticket_max"].asString().c_str(),
res_val["ticket_arr"][i]["ticket_count"].asString().c_str(),
res_val["ticket_arr"][i]["day_time"].asString().c_str());
}
cout<<"|----------|--------------|--------|--------|------------|"<<endl;
}
void Client::Yd_Ticket(){
Show_Ticket();
cout<<"请输入需要预订的序号"<<endl;
int index;
cin>>index;
Json::Value val;
val["type"] = Appointment;
val["user_tel"] = user_tel;
val["ticket_id"] = to_string(index);
send(conn_fd,val.toStyledString().c_str(),strlen(val.toStyledString().c_str()),0);
char buff[Length] = {0};
int n = recv(conn_fd,buff,Length-1,0);
if(n<0){
cout<<"ser close"<<endl;
return ;
}
Json::Value res_val;
Json::Reader Read;
if(!Read.parse(buff,res_val)){
cout<<"Json 解析失败"<<endl;
return;
}
string s = res_val["status"].asString();
if(s == BUSY){
cout<<"服务器繁忙,请稍后重试"<<endl;
return;
}
if(s.compare(k)!=0){
cout<<"预订失败"<<endl;
return;
}
cout<<"预订成功!"<<endl;
}
void Client::Show_My_Ticket(){
Json::Value val;
val["type"] = AppoinMessage;
val["user_tel"] = user_tel;
send(conn_fd,val.toStyledString().c_str(),strlen(val.toStyledString().c_str()),0);
char buff[Length]={0};
int n = recv(conn_fd,buff,Length,0);
if(n<=0){
cout<<"ser close"<<endl;
return;
}
Json::Value res_val;
Json::Reader Read;
if(!Read.parse(buff,res_val)){
cout<<"Json 解析失败"<<endl;
return;
}
string s = res_val["status"].asString();
if(s == BUSY){
cout<<"服务器繁忙,请稍后重试"<<endl;
return;
}
if(s.compare(k)!=0){
cout<<"查看我的预约失败"<<endl;
return;
}
int num = res_val["num"].asInt();
if(num == 0){
cout<<"您还没有预约任何信息"<<endl;
return ;
}
cout<<"|----------|--------------|--------|--------|------------|"<<endl;
cout<<"| 预约ID | 名称 | 总票数 | 已预定 | 时间 |"<<endl;
cout<<"|----------|--------------|--------|--------|------------|"<<endl;
for(int i=0;i<num;i++){
printf("| %8s | %12s | %6s | %6s | %10s |\n",
res_val["my_ticket_arr"][i]["yd_id"].asString().c_str(),
res_val["my_ticket_arr"][i]["ticket_name"].asString().c_str(),
res_val["my_ticket_arr"][i]["ticket_max"].asString().c_str(),
res_val["my_ticket_arr"][i]["ticket_count"].asString().c_str(),
res_val["my_ticket_arr"][i]["day_time"].asString().c_str());
}
cout<<"|----------|--------------|--------|--------|------------|"<<endl;
}
void Client::Qx_Ticket(){
Show_My_Ticket();
cout<<"请输入需要取消的预约ID:"<<endl;
string yd_id;
cin>>yd_id;
Json::Value val;
val["type"] = CancelAppoin;
val["user_tel"] = user_tel;
val["yd_id"] = yd_id;
send(conn_fd,val.toStyledString().c_str(),strlen(val.toStyledString().c_str()),0);
char buff[Length] = {0};
int n = recv(conn_fd,buff,Length-1,0);
if(n <= 0){
cout<<"ser close"<<endl;
return ;
}
Json::Value res_val;
Json::Reader Read;
if(!Read.parse(buff,res_val)){
cout<<"Json解析失败"<<endl;
return ;
}
string s = res_val["status"].asString();
if(s == BUSY){
cout<<"服务器繁忙,请稍后重试"<<endl;
return ;
}
if(s.compare(k)!=0){
cout<<"取消预约失败"<<endl;
return ;
}
cout<<"取消预约成功"<<endl;
}
void Client::Run(){
while(Running){
Show_Menu();
int tmp;
cin>>tmp;
if(tmp < Login || tmp > Exit){
cout<<"输入错误"<<endl;
continue;
}
if(Dl_Status){
tmp = tmp+2;
}
else if(tmp==3){
tmp = Exit;
}
switch(tmp){
case Login:
User_login();
break;
case Register:
User_Register();
break;
case CheckApoint:
Show_Ticket();
break;
case Appointment:
Yd_Ticket();
break;
case AppoinMessage:
Show_My_Ticket();
break;
case CancelAppoin:
Qx_Ticket();
break;
case Exit:
Running = false;
break;
}
}
}
int main(){
Client client;
return 0;
}
文件作用
实现 Client 类所有方法 + main(约 410 行)。
通信协议(JSON)
客户端发送:
json
{ "type": 1, "user_tel": "...", "passwd": "...", "user_name": "..." }
服务端返回(成功):
json
{ "status": "ok", "num": 3, "ticket_arr": [...] }
服务端返回(失败):
json
{ "status": "false" }
服务端返回(池满):
json
{ "status": "MYSQL CLIENT BUSSY" }
6 个业务方法
| 方法 | type | 流程 |
|---|---|---|
User_Register |
2 | 输入 tel/name/passwd → send → recv → BUSY?/ok? |
User_login |
1 | 同上 |
Show_Ticket |
3 | 不带参数 → recv 列表 → 表格打印 |
Yd_Ticket |
4 | 先 Show_Ticket → 输入序号 → send → recv |
Show_My_Ticket |
5 | 带 user_tel → recv → 表格 |
Qx_Ticket |
6 | 先 Show_My_Ticket → 输入 yd_id → send → recv |
菜单逻辑
cpp
switch(tmp) {
case Login: User_login(); break;
case Register: User_Register(); break;
...
case Exit: Running = false; break;
}
设计亮点
- 统一 BUSY 检查模板:6 个方法都先判断
s == BUSY提示"服务器繁忙",再判断s != k提示具体失败 - 业务方法可组合(Yd_Ticket 复用 Show_Ticket,Qx_Ticket 复用 Show_My_Ticket)
不足
recv阻塞,无超时(服务端挂起客户端会一直等)cin >> tmp没检查失败状态(输入非数字会陷入死循环)- 表格用
printf硬编码列宽,长名会错位 - 全局 enum
OP_TYPE与服务端重复定义(应放共享头)
4.5 --- 构建脚本
完整代码
makefile
all:service client
service:service.cpp
g++ -g -O0 -o service service.cpp -lpthread -lmysqlclient -ljsoncpp
client:client.cpp
g++ -g -O0 -o client client.cpp -lpthread -lmysqlclient -ljsoncpp
clean:
rm -f *.o service client
依赖说明
| 库 | 用途 |
|---|---|
-lpthread |
pthread_t / mutex / cond |
-lmysqlclient |
MySQL C API |
-ljsoncpp |
JSON 解析 |
改进建议
- 加
-Wall -Wextra开启警告 - 加
-std=c++11(用了auto、nullptr) - 拆分头文件后改用
.o中间产物 + 自动依赖
4.6 <ser_start.sh> --- 启动脚本
完整代码
bash
#!/bin/bash
if [ -f Makefile ]
then
make clean
make all
fi
if [ -f service ]
then
./service service.conf
fi
不足
- 没加
set -e,make 失败仍会启动旧 service - 没传
service.conf路径给程序读(代码里也硬编码了) - 没用
nohup后台运行 + 日志重定向
4.7 <service.conf> --- 服务端配置
完整内容
conf
ip=127.0.0.1
port=8888
lismax=1024
service_conf_path=/home/xiaoc/mycode/csproject/service.conf
注意
service_conf_path字段在代码里赋值给局部参数,无实际作用- 修改后必须 Ctrl+S 保存磁盘才生效(之前 IDE 缓冲区与磁盘不一致问题)
4.8 <mysql.conf> --- 数据库配置
完整内容
conf
dp_ip=127.0.0.1
dp_port=3306
dp_name=c2411db
dp_mysql_path=/home/xiaoc/mycode/csproject/mysql.conf
注意
dp_mysql_path路径写错(少了 "2"),但代码里赋值给局部参数,无实际作用- 缺少
dp_user/dp_password字段,密码硬编码在 <service.cpp#L787>
4.9 <web/cp_client.py> --- C++ TCP 服务 Python 客户端封装
封装与 C++ 服务端(127.0.0.1:9999)的 TCP 通信,严格对齐 C++ JSON 协议。每个业务方法新建一条 TCP 连接:发送请求 → 等待响应 → 关闭连接,与原 C++ 客户端行为一致。
完整代码
python
# -*- coding: utf-8 -*-
"""
与 C++ 预约系统服务端 (127.0.0.1:9999) 通信的客户端封装。
C++ 服务端协议 (JSON over TCP):
请求字段:
- type: 操作类型 (1=Login,2=Register,3=CheckApoint,
4=Appointment,5=AppoinMessage,6=CancelAppoin)
- user_tel: 手机号
- user_name: 用户名
- passwd: 密码
- ticket_id: 票 id (字符串)
- yd_id: 预约 id (字符串)
响应字段:
- status: "ok" / "false" / "MYSQL CLIENT BUSSY"
- num: 记录数 (查询类)
- ticket_arr: 票数组 (查看可预约)
- my_ticket_arr: 我的预约数组
"""
import socket
import json
import time
# 与 C++ 端 enum OP_TYPE 完全一致
OP_LOGIN = 1
OP_REGISTER = 2
OP_CHECK_APOINT = 3
OP_APPOINTMENT = 4
OP_APPOIN_MESSAGE = 5
OP_CANCEL_APPOIN = 6
# 状态码常量
STATUS_OK = "ok"
STATUS_FALSE = "false"
STATUS_BUSY = "MYSQL CLIENT BUSSY"
class CPClientError(Exception):
"""与 C++ 服务通信相关的异常"""
class CPClient(object):
"""对 C++ 预约系统服务端的轻量 TCP 客户端。
每个业务方法都会新建一条 TCP 连接:发送请求 -> 等待响应 -> 关闭连接。
这与原 C++ 客户端行为一致,也避免长连接状态管理问题。
"""
def __init__(self, host="127.0.0.1", port=9999,
connect_timeout=3.0, recv_timeout=5.0):
self.host = host
self.port = port
self.connect_timeout = connect_timeout
self.recv_timeout = recv_timeout
# ----------------------------------------------------------------- 底层
def _round_trip(self, req_obj):
"""发送一个 JSON 请求并接收完整 JSON 响应。
返回解析后的 dict。若连接失败、超时或 JSON 解析失败,抛 CPClientError。
"""
payload = json.dumps(req_obj, ensure_ascii=False)
data = payload.encode("utf-8")
# C++ 服务端用 buff[MAXLINE=256] recv,请求超长会被截断导致解析失败
if len(data) >= 256:
raise CPClientError(
"请求过长 (%d 字节),C++ 服务端 MAXLINE=256,请缩短输入" % len(data)
)
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(self.connect_timeout)
sock.connect((self.host, self.port))
except socket.error as e:
raise CPClientError(
"无法连接 C++ 服务 %s:%d,请确认 ./service 已启动 (%s)"
% (self.host, self.port, e)
)
try:
sock.settimeout(self.recv_timeout)
sock.sendall(data)
chunks = []
total = 0
# 循环 recv,直到拿到完整 JSON 或连接关闭/超时
# 服务端响应是 toStyledString,可能大于一个 TCP 报文
while True:
try:
chunk = sock.recv(4096)
except socket.timeout:
# 接收空闲,尝试解析已收到的数据
break
if not chunk:
# 对端关闭,结束接收
break
chunks.append(chunk)
total += len(chunk)
# 尝试解析,成功则提前结束
if self._try_parse(chunks) is not None:
break
# 防御性上限,避免异常情况下无限增长
if total >= 64 * 1024:
break
raw = b"".join(chunks)
if not raw:
raise CPClientError("C++ 服务未返回任何数据")
parsed = self._try_parse([raw])
if parsed is None:
raise CPClientError(
"C++ 服务返回的不是有效 JSON:%r" % raw[:200]
)
return parsed
finally:
try:
sock.close()
except Exception:
pass
@staticmethod
def _try_parse(chunks):
try:
text = b"".join(chunks).decode("utf-8", errors="strict")
return json.loads(text)
except (ValueError, UnicodeDecodeError):
return None
# ----------------------------------------------------------- 业务方法
def register(self, user_tel, user_name, passwd):
"""用户注册"""
resp = self._round_trip({
"type": OP_REGISTER,
"user_tel": user_tel,
"user_name": user_name,
"passwd": passwd,
})
return self._normalize_status(resp), resp
def login(self, user_tel, user_name, passwd):
"""用户登录"""
resp = self._round_trip({
"type": OP_LOGIN,
"user_tel": user_tel,
"user_name": user_name,
"passwd": passwd,
})
return self._normalize_status(resp), resp
def show_tickets(self):
"""查看可预约的票列表"""
resp = self._round_trip({"type": OP_CHECK_APOINT})
return self._normalize_status(resp), resp
def appoint(self, user_tel, ticket_id):
"""预定一张票"""
resp = self._round_trip({
"type": OP_APPOINTMENT,
"user_tel": user_tel,
"ticket_id": str(ticket_id),
})
return self._normalize_status(resp), resp
def show_my_tickets(self, user_tel):
"""查看我的预约"""
resp = self._round_trip({
"type": OP_APPOIN_MESSAGE,
"user_tel": user_tel,
})
return self._normalize_status(resp), resp
def cancel(self, user_tel, yd_id):
"""取消预约"""
resp = self._round_trip({
"type": OP_CANCEL_APPOIN,
"user_tel": user_tel,
"yd_id": str(yd_id),
})
return self._normalize_status(resp), resp
# ----------------------------------------------------------- 辅助
@staticmethod
def _normalize_status(resp):
"""把服务端 status 字段归一化为 (ok/false/busy) 字符串"""
if not isinstance(resp, dict):
return STATUS_FALSE
status = resp.get("status")
if status == STATUS_OK:
return STATUS_OK
if status == STATUS_BUSY:
return STATUS_BUSY
# false 或其它都视为失败
return STATUS_FALSE
if __name__ == "__main__":
# 自测:直接运行会尝试连服务
c = CPClient()
try:
ok, resp = c.show_tickets()
print("show_tickets ->", ok)
print(json.dumps(resp, ensure_ascii=False, indent=2))
except CPClientError as e:
print("ERR:", e)
文件作用
桥接 Flask 后端与 C++ TCP 服务。把 Web 层的 HTTP 请求翻译成 C++ 服务端能理解的 JSON-over-TCP 协议,并把 C++ 返回的 status 字段归一化为 ok/false/busy。
关键设计
- type 整数常量 :与 C++
enum OP_TYPE一一对应 - 循环 recv + 即时解析 :解决 C++
toStyledString响应可能跨多个 TCP 报文的问题(比原 C++ 客户端单次 recv 更健壮) - 请求长度检查 :C++ 端
MAXLINE=256,超长请求会在 Python 端直接拒绝,避免 C++ 端截断后 parse 失败 - finally 关闭 socket:保证连接不泄漏
4.10 <web/app.py> --- Flask Web 后端
提供 9 个路由:2 个页面(登录页、主页)+ 1 个登出 + 6 个 JSON API。用 Flask session 维持登录态,通过 CPClient 转发请求到 C++ 服务。
完整代码
python
# -*- coding: utf-8 -*-
"""
预约系统 Web 可视化界面 - Flask 后端
职责:
1. 提供网页 (templates/...)
2. 提供 JSON API,转发到 C++ TCP 服务 (127.0.0.1:9999)
3. 用 Flask session 维持登录态
启动:
python3 app.py
或: ./start.sh
默认监听 0.0.0.0:5000,浏览器访问 http://localhost:5000
"""
import os
import functools
from flask import (Flask, render_template, request, session,
jsonify, redirect, url_for, abort)
from cp_client import CPClient, CPClientError, STATUS_OK, STATUS_BUSY
app = Flask(__name__)
app.config["SECRET_KEY"] = os.environ.get(
"APPOINT_SECRET_KEY", "dev-secret-please-change-in-prod"
)
# C++ 服务地址,可按需通过环境变量覆盖
CP_HOST = os.environ.get("CP_HOST", "127.0.0.1")
CP_PORT = int(os.environ.get("CP_PORT", "9999"))
cp = CPClient(host=CP_HOST, port=CP_PORT)
# ----------------------------------------------------------------- 工具
def login_required(view):
"""要求已登录才能访问的视图装饰器"""
@functools.wraps(view)
def wrapped(*args, **kwargs):
if not session.get("user_tel"):
# AJAX 请求返回 401,浏览器请求重定向到登录页
if request.path.startswith("/api/"):
return jsonify(ok=False, msg="请先登录"), 401
return redirect(url_for("login"))
return view(*args, **kwargs)
return wrapped
def api_err(msg, status=400):
return jsonify(ok=False, msg=msg), status
def api_busy():
return jsonify(ok=False, msg="服务器繁忙,请稍后重试"), 503
# ----------------------------------------------------------------- 页面
@app.route("/")
def index():
"""主页:未登录跳登录页;已登录渲染主界面"""
if not session.get("user_tel"):
return redirect(url_for("login"))
return render_template("index.html", user_name=session.get("user_name", ""))
@app.route("/login")
def login():
"""登录 / 注册页"""
# 已登录直接进主页
if session.get("user_tel"):
return redirect(url_for("index"))
return render_template("login.html")
@app.route("/logout", methods=["POST"])
def logout():
session.clear()
return jsonify(ok=True)
# ----------------------------------------------------------------- API
@app.route("/api/register", methods=["POST"])
def api_register():
data = request.get_json(silent=True) or {}
tel = (data.get("user_tel") or "").strip()
name = (data.get("user_name") or "").strip()
passwd = (data.get("passwd") or "").strip()
if not (tel and name and passwd):
return api_err("手机号、用户名、密码都不能为空")
if len(passwd) < 4:
return api_err("密码至少 4 位")
try:
status, resp = cp.register(tel, name, passwd)
except CPClientError as e:
return api_err(str(e), 502)
if status == STATUS_BUSY:
return api_busy()
if status != STATUS_OK:
return api_err("注册失败,该手机号可能已存在")
# 注册成功后视作已登录(与 C++ 客户端行为一致)
session["user_tel"] = tel
session["user_name"] = name
return jsonify(ok=True, msg="注册成功")
@app.route("/api/login", methods=["POST"])
def api_login():
data = request.get_json(silent=True) or {}
tel = (data.get("user_tel") or "").strip()
name = (data.get("user_name") or "").strip()
passwd = (data.get("passwd") or "").strip()
if not (tel and name and passwd):
return api_err("手机号、用户名、密码都不能为空")
try:
status, resp = cp.login(tel, name, passwd)
except CPClientError as e:
return api_err(str(e), 502)
if status == STATUS_BUSY:
return api_busy()
if status != STATUS_OK:
return api_err("用户名或密码错误")
session["user_tel"] = tel
session["user_name"] = name
return jsonify(ok=True, msg="登录成功")
@app.route("/api/tickets", methods=["GET"])
@login_required
def api_tickets():
"""查看可预约的票"""
try:
status, resp = cp.show_tickets()
except CPClientError as e:
return api_err(str(e), 502)
if status == STATUS_BUSY:
return api_busy()
if status != STATUS_OK:
return api_err("查看预约信息失败")
num = resp.get("num", 0)
arr = resp.get("ticket_arr", []) or []
return jsonify(ok=True, num=num, tickets=arr)
@app.route("/api/appoint", methods=["POST"])
@login_required
def api_appoint():
"""预定一张票"""
data = request.get_json(silent=True) or {}
ticket_id = (data.get("ticket_id") or "").strip()
if not ticket_id:
return api_err("缺少 ticket_id")
try:
status, resp = cp.appoint(session["user_tel"], ticket_id)
except CPClientError as e:
return api_err(str(e), 502)
if status == STATUS_BUSY:
return api_busy()
if status != STATUS_OK:
return api_err("预定失败,票数可能已满")
return jsonify(ok=True, msg="预定成功")
@app.route("/api/my_tickets", methods=["GET"])
@login_required
def api_my_tickets():
"""查看我的预约"""
try:
status, resp = cp.show_my_tickets(session["user_tel"])
except CPClientError as e:
return api_err(str(e), 502)
if status == STATUS_BUSY:
return api_busy()
if status != STATUS_OK:
return api_err("查看我的预约失败")
num = resp.get("num", 0)
arr = resp.get("my_ticket_arr", []) or []
return jsonify(ok=True, num=num, tickets=arr)
@app.route("/api/cancel", methods=["POST"])
@login_required
def api_cancel():
"""取消预定"""
data = request.get_json(silent=True) or {}
yd_id = (data.get("yd_id") or "").strip()
if not yd_id:
return api_err("缺少 yd_id")
try:
status, resp = cp.cancel(session["user_tel"], yd_id)
except CPClientError as e:
return api_err(str(e), 502)
if status == STATUS_BUSY:
return api_busy()
if status != STATUS_OK:
return api_err("取消预约失败")
return jsonify(ok=True, msg="取消预约成功")
# ----------------------------------------------------------------- 入口
@app.route("/api/me", methods=["GET"])
@login_required
def api_me():
return jsonify(ok=True,
user_tel=session.get("user_tel", ""),
user_name=session.get("user_name", ""))
if __name__ == "__main__":
print("=" * 60)
print("预约系统 Web 界面启动中")
print("C++ 服务地址: %s:%d" % (CP_HOST, CP_PORT))
print("Web 监听: http://0.0.0.0:5000")
print("浏览器请访问: http://localhost:5000")
print("=" * 60)
# debug=False 避免多进程导致 C++ 连接被并发打开
app.run(host="0.0.0.0", port=5000, debug=False, threaded=True)
路由一览
| 方法 | 路径 | 鉴权 | 说明 |
|---|---|---|---|
| GET | / |
--- | 主页(未登录跳 /login) |
| GET | /login |
--- | 登录/注册页(已登录跳 /) |
| POST | /logout |
--- | 清除 session |
| POST | /api/register |
--- | 注册(成功后自动登录) |
| POST | /api/login |
--- | 登录(写入 session) |
| GET | /api/tickets |
✓ | 查看可预约票 |
| POST | /api/appoint |
✓ | 预定票 |
| GET | /api/my_tickets |
✓ | 查看我的预约 |
| POST | /api/cancel |
✓ | 取消预约 |
| GET | /api/me |
✓ | 获取当前用户 |
关键设计
- session 登录态 :登录/注册成功后
session["user_tel"]写入手机号,@login_required装饰器校验 - 鉴权分流:AJAX 请求未登录返回 401 JSON,浏览器请求未登录 302 重定向
- C++ 繁忙处理 :
status == STATUS_BUSY返回 HTTP 503,前端提示稍后重试 - C++ 连接失败处理 :
CPClientError返回 HTTP 502,前端提示服务未启动
4.11 <web/templates/login.html> --- 登录 / 注册页
居中卡片式表单,双 Tab 切换登录/注册,三字段(手机号 + 用户名 + 密码)。
完整代码
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>预约系统 · 登录</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body class="auth-page">
<div class="auth-card">
<div class="auth-header">
<h1>预约系统</h1>
<p>请登录或注册后进入</p>
</div>
<!-- 标签切换 -->
<div class="tabs">
<button type="button" class="tab-btn active" data-tab="login">登录</button>
<button type="button" class="tab-btn" data-tab="register">注册</button>
</div>
<!-- 登录表单 -->
<form id="login-form" class="auth-form active" autocomplete="off">
<label>手机号
<input type="text" name="user_tel" placeholder="11 位手机号" required>
</label>
<label>用户名
<input type="text" name="user_name" placeholder="用户名" required>
</label>
<label>密码
<input type="password" name="passwd" placeholder="密码" required>
</label>
<button type="submit" class="btn-primary">登录</button>
</form>
<!-- 注册表单 -->
<form id="register-form" class="auth-form" autocomplete="off">
<label>手机号
<input type="text" name="user_tel" placeholder="11 位手机号" required>
</label>
<label>用户名
<input type="text" name="user_name" placeholder="用户名" required>
</label>
<label>密码
<input type="password" name="passwd" placeholder="密码(≥4 位)" required>
</label>
<button type="submit" class="btn-primary">注册</button>
</form>
<div id="auth-msg" class="auth-msg"></div>
</div>
<script src="{{ url_for('static', filename='app.js') }}"></script>
</body>
</html>
关键元素
.tab-btn[data-tab]:Tab 切换按钮,data-tab值为login/register#login-form/#register-form:两个表单,通过.activeclass 切换显示#auth-msg:表单下方提示区(成功绿色 / 失败红色)- 表单字段 name 与后端 API 完全一致:
user_tel/user_name/passwd
4.12 <web/templates/index.html> --- 主操作页
顶部导航栏 + 左侧栏 + 右侧内容区,含可预约票表和我的预约表。
完整代码
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>预约系统 · 主页</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body class="app-page">
<header class="app-header">
<div class="brand">
<span class="logo">预约</span>
<span class="brand-text">预约系统</span>
</div>
<div class="user-area">
<span class="user-tag">用户名:<b id="nav-user-name">{{ user_name }}</b></span>
<span class="user-tag">手机号:<b id="nav-user-tel"></b></span>
<button type="button" class="btn-ghost" id="btn-logout">退出</button>
</div>
</header>
<main class="app-body">
<!-- 侧栏 -->
<nav class="sidebar">
<button type="button" class="nav-item active" data-view="tickets">
查看可预约
</button>
<button type="button" class="nav-item" data-view="mine">
我的预约
</button>
</nav>
<!-- 内容区 -->
<section class="content">
<div class="content-head">
<h2 id="view-title">可预约的票</h2>
<button type="button" class="btn-primary" id="btn-refresh">刷新</button>
</div>
<!-- 通用提示 -->
<div id="global-msg" class="toast"></div>
<!-- 可预约表 -->
<div id="view-tickets" class="view active">
<table class="data-table">
<thead>
<tr>
<th>序号</th><th>名称</th><th>总票数</th>
<th>已预定</th><th>剩余</th><th>时间</th><th>操作</th>
</tr>
</thead>
<tbody id="tb-tickets"></tbody>
</table>
</div>
<!-- 我的预约表 -->
<div id="view-mine" class="view">
<table class="data-table">
<thead>
<tr>
<th>预约ID</th><th>名称</th><th>总票数</th>
<th>已预定</th><th>时间</th><th>操作</th>
</tr>
</thead>
<tbody id="tb-mine"></tbody>
</table>
</div>
</section>
</main>
<script src="{{ url_for('static', filename='app.js') }}"></script>
</body>
</html>
关键元素
.app-header:顶部导航栏(品牌 + 用户名/手机号 + 退出按钮).nav-item[data-view]:侧栏导航项,data-view值为tickets/mine#view-tickets/#view-mine:两个视图容器,通过.activeclass 切换#tb-tickets/#tb-mine:表格 tbody,由 JS 动态渲染#btn-refresh:刷新按钮,重新加载当前视图{``{ user_name }}:Flask 模板变量,后端直接渲染用户名
4.13 <web/static/style.css> --- 页面样式
现代化卡片式 UI,含 CSS 变量主题色、阴影、响应式布局(移动端侧栏收窄)。
完整代码
css
/* ============================================================
预约系统 Web 界面 - 样式
============================================================ */
:root {
--primary: #4f7cff;
--primary-dark: #3a63d8;
--danger: #e2536b;
--bg: #f5f7fb;
--card: #ffffff;
--border: #e3e8f0;
--text: #1f2a44;
--text-sub: #6b7896;
--shadow: 0 6px 24px rgba(31, 42, 68, .08);
}
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI",
"PingFang SC", "Microsoft YaHei", sans-serif;
color: var(--text);
background: var(--bg);
font-size: 14px;
line-height: 1.5;
}
button { font-family: inherit; cursor: pointer; }
/* ---------------- 登录/注册页 ---------------- */
.auth-page {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #6a8dff 0%, #4f7cff 50%, #2a4db8 100%);
}
.auth-card {
width: 380px;
max-width: 92vw;
background: var(--card);
border-radius: 14px;
box-shadow: var(--shadow);
padding: 28px 28px 24px;
}
.auth-header {
text-align: center;
margin-bottom: 18px;
}
.auth-header h1 {
margin: 0 0 4px;
font-size: 22px;
font-weight: 700;
color: var(--text);
}
.auth-header p {
margin: 0;
color: var(--text-sub);
font-size: 13px;
}
.tabs {
display: flex;
background: #eef2fb;
border-radius: 8px;
padding: 4px;
margin-bottom: 18px;
}
.tab-btn {
flex: 1;
border: none;
background: transparent;
padding: 8px 0;
font-size: 14px;
color: var(--text-sub);
border-radius: 6px;
transition: all .2s;
}
.tab-btn.active {
background: var(--card);
color: var(--text);
font-weight: 600;
box-shadow: 0 2px 8px rgba(0, 0, 0, .06);
}
.auth-form {
display: none;
flex-direction: column;
gap: 12px;
}
.auth-form.active { display: flex; }
.auth-form label {
display: flex;
flex-direction: column;
font-size: 13px;
color: var(--text-sub);
gap: 4px;
}
.auth-form input {
height: 40px;
padding: 0 12px;
border: 1px solid var(--border);
border-radius: 8px;
font-size: 14px;
color: var(--text);
background: #fbfcfe;
outline: none;
transition: border-color .15s, box-shadow .15s;
}
.auth-form input:focus {
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(79, 124, 255, .15);
background: #fff;
}
.btn-primary {
height: 40px;
border: none;
border-radius: 8px;
background: var(--primary);
color: #fff;
font-size: 14px;
font-weight: 600;
transition: background .15s;
}
.btn-primary:hover { background: var(--primary-dark); }
.btn-primary:active { transform: translateY(1px); }
.btn-primary:disabled { background: #aab4cf; cursor: not-allowed; }
.btn-ghost {
height: 34px;
padding: 0 14px;
border: 1px solid var(--border);
border-radius: 8px;
background: #fff;
color: var(--text);
font-size: 13px;
transition: background .15s;
}
.btn-ghost:hover { background: #f0f3fa; }
.auth-msg {
margin-top: 12px;
font-size: 13px;
min-height: 18px;
text-align: center;
}
.auth-msg.ok { color: #1f9d57; }
.auth-msg.err { color: var(--danger); }
/* ---------------- 主页 ---------------- */
.app-page {
min-height: 100vh;
}
.app-header {
height: 56px;
background: var(--card);
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20px;
box-shadow: 0 1px 4px rgba(0, 0, 0, .04);
}
.brand {
display: flex;
align-items: center;
gap: 10px;
}
.logo {
width: 30px;
height: 30px;
background: var(--primary);
color: #fff;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-size: 13px;
font-weight: 700;
}
.brand-text {
font-size: 16px;
font-weight: 700;
}
.user-area {
display: flex;
align-items: center;
gap: 14px;
}
.user-tag {
font-size: 13px;
color: var(--text-sub);
}
.user-tag b { color: var(--text); }
.app-body {
display: flex;
min-height: calc(100vh - 56px);
}
.sidebar {
width: 200px;
background: var(--card);
border-right: 1px solid var(--border);
padding: 12px 10px;
display: flex;
flex-direction: column;
gap: 4px;
}
.nav-item {
text-align: left;
border: none;
background: transparent;
padding: 10px 14px;
font-size: 14px;
color: var(--text-sub);
border-radius: 8px;
transition: background .15s, color .15s;
}
.nav-item:hover { background: #f0f3fa; color: var(--text); }
.nav-item.active {
background: rgba(79, 124, 255, .12);
color: var(--primary);
font-weight: 600;
}
.content {
flex: 1;
padding: 20px 24px;
overflow: auto;
}
.content-head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 14px;
}
.content-head h2 {
margin: 0;
font-size: 18px;
font-weight: 600;
}
.content-head .btn-primary { min-width: 80px; }
/* 表格 */
.data-table {
width: 100%;
border-collapse: collapse;
background: var(--card);
border-radius: 10px;
overflow: hidden;
box-shadow: var(--shadow);
}
.data-table th,
.data-table td {
padding: 12px 14px;
text-align: left;
font-size: 14px;
border-bottom: 1px solid var(--border);
}
.data-table thead th {
background: #f7f9fd;
color: var(--text-sub);
font-weight: 600;
font-size: 13px;
}
.data-table tbody tr:last-child td { border-bottom: none; }
.data-table tbody tr:hover { background: #fafcff; }
.empty-row td {
text-align: center;
color: var(--text-sub);
padding: 30px 0;
}
.row-full { color: var(--danger); font-weight: 600; }
.row-available { color: #1f9d57; }
.btn-action {
height: 32px;
padding: 0 14px;
border: none;
border-radius: 6px;
font-size: 13px;
font-weight: 600;
color: #fff;
background: var(--primary);
transition: background .15s;
}
.btn-action:hover { background: var(--primary-dark); }
.btn-action.danger { background: var(--danger); }
.btn-action.danger:hover { background: #c63f55; }
.btn-action:disabled { background: #aab4cf; cursor: not-allowed; }
/* 视图切换 */
.view { display: none; }
.view.active { display: block; }
/* Toast 提示 */
.toast {
display: none;
margin-bottom: 14px;
padding: 10px 14px;
border-radius: 8px;
font-size: 13px;
}
.toast.show { display: block; }
.toast.ok {
background: #e6f7ee;
color: #15703e;
border: 1px solid #b6e3c6;
}
.toast.err {
background: #fdeef1;
color: #b03247;
border: 1px solid #f6c8d1;
}
@media (max-width: 720px) {
.sidebar { width: 64px; padding: 8px 6px; }
.nav-item { padding: 10px 8px; font-size: 12px; }
.content { padding: 14px; }
.user-tag { display: none; }
}
关键样式
- CSS 变量主题色 :
:root定义--primary/--danger等,统一色调 - 登录页 :渐变背景 + 居中卡片,Tab 切换用
.activeclass - 主页:flex 布局(侧栏 + 内容区),表格圆角阴影
- 响应式 :
@media (max-width: 720px)侧栏收窄到 64px,隐藏用户标签
4.14 <web/static/app.js> --- 前端逻辑
IIFE 模块化封装,零外部依赖。含登录/注册表单提交、表格动态渲染、预定/取消交互、AJAX 请求封装。
完整代码
javascript
/* ============================================================
预约系统 Web 界面 - 前端逻辑
============================================================ */
(function () {
"use strict";
// --------- 工具 ---------
function qs(sel, root) { return (root || document).querySelector(sel); }
function qsa(sel, root) { return Array.prototype.slice.call((root || document).querySelectorAll(sel)); }
function showMessage(el, text, type) {
if (!el) return;
el.textContent = text || "";
el.className = "auth-msg " + (type || "");
}
function toast(text, type) {
var el = qs("#global-msg");
if (!el) return;
el.textContent = text || "";
el.className = "toast " + (type || "");
if (type) {
el.classList.add("show");
setTimeout(function () {
el.className = "toast";
}, 2500);
}
}
function fetchWithTimeout(url, opts, ms) {
var ctrl = new AbortController();
var timer = setTimeout(function () { ctrl.abort(); }, ms || 10000);
opts = opts || {};
opts.credentials = opts.credentials || "same-origin";
opts.signal = ctrl.signal;
return fetch(url, opts).finally(function () { clearTimeout(timer); });
}
function postJSON(url, body) {
return fetchWithTimeout(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body || {})
}).then(function (r) {
return r.json().then(function (d) {
return { ok: !!d.ok, status: r.status, data: d };
});
});
}
function getJSON(url) {
return fetchWithTimeout(url, { method: "GET" }).then(function (r) {
return r.json().then(function (d) {
return { ok: !!d.ok, status: r.status, data: d };
});
});
}
function netErr(err) {
if (err && err.name === "AbortError") return "请求超时:服务未响应,请确认服务已启动";
return "网络错误:服务未运行或已断开,请确认服务已启动后刷新页面重试";
}
function formData(form) {
var obj = {};
var els = form.elements;
for (var i = 0; i < els.length; i++) {
var e = els[i];
if (e.name) obj[e.name] = e.value.trim();
}
return obj;
}
// ===================== 登录 / 注册页 =====================
function initAuthPage() {
var card = qs(".auth-card");
if (!card) return;
// 标签切换
qsa(".tab-btn").forEach(function (btn) {
btn.addEventListener("click", function () {
qsa(".tab-btn").forEach(function (b) { b.classList.remove("active"); });
btn.classList.add("active");
var tab = btn.getAttribute("data-tab");
qsa(".auth-form").forEach(function (f) { f.classList.remove("active"); });
qs("#" + tab + "-form").classList.add("active");
showMessage(qs("#auth-msg"), "", "");
});
});
// 登录提交
qs("#login-form").addEventListener("submit", function (e) {
e.preventDefault();
var msg = qs("#auth-msg");
var data = formData(this);
if (!data.user_tel || !data.user_name || !data.passwd) {
return showMessage(msg, "请填写完整信息", "err");
}
var btn = qs("#login-form button[type=submit]");
btn.disabled = true; btn.textContent = "登录中...";
postJSON("/api/login", data).then(function (r) {
btn.disabled = false; btn.textContent = "登录";
if (r.ok) {
window.location.href = "/";
} else {
showMessage(msg, r.data && r.data.msg ? r.data.msg : "登录失败", "err");
}
}).catch(function () {
btn.disabled = false; btn.textContent = "登录";
showMessage(msg, "网络错误,请稍后重试", "err");
});
});
// 注册提交
qs("#register-form").addEventListener("submit", function (e) {
e.preventDefault();
var msg = qs("#auth-msg");
var data = formData(this);
if (!data.user_tel || !data.user_name || !data.passwd) {
return showMessage(msg, "请填写完整信息", "err");
}
if (data.passwd.length < 4) {
return showMessage(msg, "密码至少 4 位", "err");
}
var btn = qs("#register-form button[type=submit]");
btn.disabled = true; btn.textContent = "注册中...";
postJSON("/api/register", data).then(function (r) {
btn.disabled = false; btn.textContent = "注册";
if (r.ok) {
showMessage(msg, "注册成功,正在进入...", "ok");
setTimeout(function () { window.location.href = "/"; }, 600);
} else {
showMessage(msg, r.data && r.data.msg ? r.data.msg : "注册失败", "err");
}
}).catch(function () {
btn.disabled = false; btn.textContent = "注册";
showMessage(msg, "网络错误,请稍后重试", "err");
});
});
}
// ===================== 主页 =====================
function initAppPage() {
var page = qs(".app-page");
if (!page) return;
var currentView = "tickets";
// 侧栏切换
qsa(".nav-item").forEach(function (item) {
item.addEventListener("click", function () {
var view = item.getAttribute("data-view");
if (view === currentView) return;
qsa(".nav-item").forEach(function (b) { b.classList.remove("active"); });
item.classList.add("active");
qsa(".view").forEach(function (v) { v.classList.remove("active"); });
qs("#view-" + view).classList.add("active");
currentView = view;
qs("#view-title").textContent = (view === "tickets") ? "可预约的票" : "我的预约";
loadCurrent();
});
});
// 退出
qs("#btn-logout").addEventListener("click", function () {
postJSON("/logout", {}).then(function () {
window.location.href = "/login";
});
});
// 刷新
qs("#btn-refresh").addEventListener("click", loadCurrent);
// 加载当前视图数据,返回 promise 便于刷新后回调
function loadCurrent() {
if (currentView === "tickets") return loadTickets();
else return loadMine();
}
function loadTickets() {
var tbody = qs("#tb-tickets");
tbody.innerHTML = '<tr class="empty-row"><td colspan="7">加载中...</td></tr>';
return getJSON("/api/tickets").then(function (r) {
if (!r.ok) {
tbody.innerHTML = '<tr class="empty-row"><td colspan="7">' +
((r.data && r.data.msg) || "加载失败") + "</td></tr>";
return;
}
renderTickets(r.data.tickets || []);
}).catch(function () {
tbody.innerHTML = '<tr class="empty-row"><td colspan="7">网络错误</td></tr>';
});
}
function renderTickets(list) {
var tbody = qs("#tb-tickets");
if (!list.length) {
tbody.innerHTML = '<tr class="empty-row"><td colspan="7">暂无可预约的票</td></tr>';
return;
}
tbody.innerHTML = list.map(function (t) {
var id = (t.ticket_id !== undefined && t.ticket_id !== null) ? t.ticket_id : "";
var max = parseInt(t.ticket_max, 10) || 0;
var count = parseInt(t.ticket_count, 10) || 0;
var left = max - count;
var full = left <= 0;
var leftClass = full ? "row-full" : "row-available";
var btn = full
? '<button class="btn-action" disabled>已满</button>'
: '<button class="btn-action" data-appoint="' + id + '">预定</button>';
return '<tr>' +
'<td>' + id + '</td>' +
'<td>' + esc(t.ticket_name) + '</td>' +
'<td>' + max + '</td>' +
'<td>' + count + '</td>' +
'<td class="' + leftClass + '">' + left + '</td>' +
'<td>' + esc(t.day_time) + '</td>' +
'<td>' + btn + '</td>' +
'</tr>';
}).join("");
// 绑定预定按钮
qsa("[data-appoint]", tbody).forEach(function (b) {
b.addEventListener("click", function () {
var id = b.getAttribute("data-appoint");
b.disabled = true; b.textContent = "预定中...";
postJSON("/api/appoint", { ticket_id: id }).then(function (r) {
if (r.ok) {
loadCurrent().then(function () { alert("预定成功"); });
} else {
b.disabled = false; b.textContent = "预定";
alert((r.data && r.data.msg) || "预定失败");
}
}).catch(function (err) {
b.disabled = false; b.textContent = "预定";
alert(netErr(err));
});
});
});
}
function loadMine() {
var tbody = qs("#tb-mine");
tbody.innerHTML = '<tr class="empty-row"><td colspan="6">加载中...</td></tr>';
return getJSON("/api/my_tickets").then(function (r) {
if (!r.ok) {
tbody.innerHTML = '<tr class="empty-row"><td colspan="6">' +
((r.data && r.data.msg) || "加载失败") + "</td></tr>";
return;
}
renderMine(r.data.tickets || []);
}).catch(function () {
tbody.innerHTML = '<tr class="empty-row"><td colspan="6">服务未运行或已断开</td></tr>';
});
}
function renderMine(list) {
var tbody = qs("#tb-mine");
if (!list.length) {
tbody.innerHTML = '<tr class="empty-row"><td colspan="6">您还没有任何预约</td></tr>';
return;
}
tbody.innerHTML = list.map(function (t) {
var ydId = (t.yd_id !== undefined && t.yd_id !== null) ? t.yd_id : "";
return '<tr>' +
'<td>' + ydId + '</td>' +
'<td>' + esc(t.ticket_name) + '</td>' +
'<td>' + esc(t.ticket_max) + '</td>' +
'<td>' + esc(t.ticket_count) + '</td>' +
'<td>' + esc(t.day_time) + '</td>' +
'<td><button class="btn-action danger" data-cancel="' + ydId + '">取消</button></td>' +
'</tr>';
}).join("");
qsa("[data-cancel]", tbody).forEach(function (b) {
b.addEventListener("click", function () {
var id = b.getAttribute("data-cancel");
b.disabled = true; b.textContent = "取消中...";
postJSON("/api/cancel", { yd_id: id }).then(function (r) {
if (r.ok) {
loadCurrent().then(function () { alert("取消成功"); });
} else {
b.disabled = false; b.textContent = "取消";
alert((r.data && r.data.msg) || "取消失败");
}
}).catch(function (err) {
b.disabled = false; b.textContent = "取消";
alert(netErr(err));
});
});
});
}
// 拉一下当前用户信息(显示手机号)
getJSON("/api/me").then(function (r) {
if (r.ok && r.data) {
var tel = qs("#nav-user-tel");
if (tel && r.data.user_tel) tel.textContent = r.data.user_tel;
var name = qs("#nav-user-name");
if (name && r.data.user_name) name.textContent = r.data.user_name;
}
}).catch(function () {});
// 首次加载
loadCurrent();
}
// 简单 HTML 转义,防止内容注入
function esc(v) {
if (v === undefined || v === null) return "";
return String(v).replace(/[&<>"']/g, function (c) {
return ({
"&": "&", "<": "<", ">": ">",
'"': """, "'": "'"
})[c];
});
}
document.addEventListener("DOMContentLoaded", function () {
initAuthPage();
initAppPage();
});
})();
关键逻辑
fetchWithTimeout:基于AbortController的带超时 fetch 封装(默认 10 秒)initAuthPage:登录/注册页 Tab 切换 + 表单提交,成功后跳转/initAppPage:主页侧栏切换 + 表格渲染 + 预定/取消按钮交互loadTickets/loadMine:return Promise,便于操作成功后回调刷新 + 弹窗renderTickets:剩余 = max - count,满则禁用按钮显示「已满」esc:HTML 转义防注入
4.15 <start_all.sh> --- 一键启动脚本
检查 MySQL → 启动 C++ 服务 → 装 Flask → 启动 Web → 打开浏览器。启动时把进程 PID 写入 .service.pid / .web.pid。
完整代码
bash
#!/bin/bash
# ============================================================
# 预约系统一键启动脚本
# 用法: ./start_all.sh
# 做的事: 检查 MySQL -> 启动 C++ 服务 -> 启动 Web 服务 -> 打开浏览器
# ============================================================
set -e
ROOT=$(cd "$(dirname "$0")" && pwd)
WEB_DIR="$ROOT/web"
LOG_DIR="/tmp"
# 颜色输出
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[0;33m'
NC='\033[0m' # No Color
ok() { echo -e "${GREEN}[OK]${NC} $1"; }
fail() { echo -e "${RED}[FAIL]${NC} $1"; }
info() { echo -e "${YELLOW}[..]${NC} $1"; }
# 检查某端口是否在监听
port_listening() { ss -ltn 2>/dev/null | grep -q ":$1 "; }
# ------------------------------------------------- 1. 检查 MySQL
if ! port_listening 3306; then
fail "MySQL 未启动(3306 未监听),请先启动 MySQL 再运行本脚本"
exit 1
fi
ok "MySQL 已就绪 (3306)"
# ------------------------------------------------- 2. 启动 C++ 服务
if port_listening 9999; then
ok "C++ 服务已在运行 (9999),跳过"
else
if [ ! -x "$ROOT/service" ]; then
fail "未找到可执行文件 $ROOT/service,请先在项目根目录执行: make"
exit 1
fi
info "启动 C++ 服务..."
cd "$ROOT"
nohup ./service service.conf > "$LOG_DIR/cs_service.log" 2>&1 &
echo $! > "$ROOT/.service.pid"
sleep 2
if port_listening 9999; then
ok "C++ 服务启动成功 (9999)"
else
fail "C++ 服务启动失败,日志: $LOG_DIR/cs_service.log"
tail -n 10 "$LOG_DIR/cs_service.log" 2>/dev/null
exit 1
fi
fi
# ------------------------------------------------- 3. 启动 Web 服务
if port_listening 5000; then
ok "Web 服务已在运行 (5000),跳过"
else
if [ ! -f "$WEB_DIR/app.py" ]; then
fail "未找到 $WEB_DIR/app.py"
exit 1
fi
info "检查 Flask 依赖..."
if ! python3 -c "import flask" 2>/dev/null; then
info "Flask 未安装,正在安装..."
pip3 install flask >/dev/null 2>&1 || {
fail "Flask 安装失败,请手动执行: pip3 install flask"
exit 1
}
ok "Flask 安装完成"
fi
info "启动 Web 服务..."
cd "$WEB_DIR"
nohup python3 app.py > "$LOG_DIR/cs_web.log" 2>&1 &
echo $! > "$ROOT/.web.pid"
sleep 2
if port_listening 5000; then
ok "Web 服务启动成功 (5000)"
else
fail "Web 服务启动失败,日志: $LOG_DIR/cs_web.log"
tail -n 10 "$LOG_DIR/cs_web.log" 2>/dev/null
exit 1
fi
fi
# ------------------------------------------------- 4. 打开浏览器
URL="http://localhost:5000"
if command -v xdg-open >/dev/null 2>&1; then
xdg-open "$URL" >/dev/null 2>&1 || true
elif command -v sensible-browser >/dev/null 2>&1; then
sensible-browser "$URL" >/dev/null 2>&1 || true
fi
echo
echo "============================================================"
echo -e "${GREEN} 预约系统已启动${NC}"
echo " 浏览器访问: $URL"
echo " C++ 服务日志: $LOG_DIR/cs_service.log"
echo " Web 服务日志: $LOG_DIR/cs_web.log"
echo " 停止服务: ./stop_all.sh"
echo "============================================================"
关键逻辑
set -e:任何步骤失败立即退出- PID 文件 :
echo $! > .service.pid/.web.pid,供 stop_all.sh 精确停止 - 幂等:端口已监听则跳过,可重复执行
- 自动装 Flask :检测
import flask失败则pip3 install
4.16 <stop_all.sh> --- 一键停止脚本
优先用 PID 文件精确 kill;PID 文件不存在时按端口 9999/5000 兜底。绝不误杀其他项目(如 csproject2 的 8888 服务)。
完整代码
bash
#!/bin/bash
# ============================================================
# 预约系统一键停止脚本(只停本项目,不误杀其他项目)
# 用法: ./stop_all.sh
# 原理: 优先用 start_all.sh 写的 PID 文件精确 kill;
# PID 文件不存在时按端口 9999/5000 兜底,绝不误杀其他项目。
# ============================================================
ROOT=$(cd "$(dirname "$0")" && pwd)
SVC_PID_FILE="$ROOT/.service.pid"
WEB_PID_FILE="$ROOT/.web.pid"
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
NC='\033[0m'
ok() { echo -e "${GREEN}[OK]${NC} $1"; }
info() { echo -e "${YELLOW}[..]${NC} $1"; }
# 按端口找进程 PID(仅占用 9999/5000 的进程,不会碰 8888)
port_pid() { ss -ltnp 2>/dev/null | grep ":$1 " | grep -oP 'pid=\K[0-9]+' | head -1; }
# kill 一个 PID(先 TERM 后 KILL)
kill_pid() {
local pid=$1 name=$2
if [ -z "$pid" ]; then
ok "$name 未运行,跳过"
return
fi
if ! kill -0 "$pid" 2>/dev/null; then
ok "$name PID $pid 已不存在"
return
fi
info "停止 $name: PID $pid"
kill "$pid" 2>/dev/null
sleep 1
if kill -0 "$pid" 2>/dev/null; then
kill -9 "$pid" 2>/dev/null || true
fi
ok "$name 已停止"
}
# ---- 停止 Web 服务 ----
WEB_PID=""
if [ -f "$WEB_PID_FILE" ]; then
WEB_PID=$(cat "$WEB_PID_FILE" 2>/dev/null)
fi
if [ -z "$WEB_PID" ]; then
WEB_PID=$(port_pid 5000) # PID 文件不存在时按端口兜底
fi
kill_pid "$WEB_PID" "Web 服务"
rm -f "$WEB_PID_FILE"
# ---- 停止 C++ 服务 ----
SVC_PID=""
if [ -f "$SVC_PID_FILE" ]; then
SVC_PID=$(cat "$SVC_PID_FILE" 2>/dev/null)
fi
if [ -z "$SVC_PID" ]; then
SVC_PID=$(port_pid 9999) # PID 文件不存在时按端口兜底
fi
kill_pid "$SVC_PID" "C++ 服务"
rm -f "$SVC_PID_FILE"
# ---- 确认端口已释放 ----
sleep 1
echo
if ! ss -ltn 2>/dev/null | grep -qE ':9999|:5000'; then
ok "端口 9999 / 5000 已全部释放"
else
info "仍有端口占用,请检查: ss -ltn | grep -E ':9999|:5000'"
fi
关键逻辑
- PID 文件优先 :读
.service.pid/.web.pid精确 kill,避免误杀 - 端口兜底 :
ss -ltnp按 9999/5000 找 PID,只针对本项目端口 - 不碰 8888:csproject2 的服务在 8888,本脚本完全不涉及该端口
- 先 TERM 后 KILL :
kill优雅停止,1 秒后仍存活则kill -9
4.17 <web/requirements.txt> --- Python 依赖
完整内容
flask>=2.0
说明
- flask>=2.0:Web 框架,用于路由、session、JSON 响应
- 无其他依赖 :
cp_client.py仅用标准库socket/json/time,前端纯原生 JS 无框架 - 安装 :
pip3 install -r requirements.txt(start_all.sh 已自动检测并安装)
五、Bug 修复记录(本次版本)
| Bug | 描述 | 状态 |
|---|---|---|
| 1 | Connect_Mysql_Server 中 i 不递归导致死循环 |
✅ 已修 |
| 2 | Run() 没把 mysql 保存到 C_P_arrayi.mysql_con |
✅ 已修 |
| 3 | status 语义反(true 应表示可用) | ✅ 已修 |
| 4 | 借出后没设 status=false | ✅ 已修 |
| 5 | Mysql_Client 构造失败仍执行 SQL | ✅ 已修 |
| 6 | Thread_Pool 构造 delete this |
✅ 已修 |
| 7 | 借/还不加锁 | ✅ 已修 |
| A | Mysql_Client::mysql_con 构造时未初始化为 nullptr | ✅ 已修 |
| B | day-time vs day_time 字段名不匹配 |
✅ 已修 |
| C | Connect_Pool::Run 失败时局部 mysql 指针丢失 | ⚠️ 不修(OS 兜底) |
| D | Tcp_Service::Init 局部 file 遮蔽全局 | ✅ 已修 |
Web 版本新增修复(支撑 Web 界面稳定运行):
| Bug | 描述 | 状态 |
|---|---|---|
| E | main() 未忽略 SIGPIPE,客户端提前关闭连接时 send() 触发 SIGPIPE 杀死 service 进程(service 反复崩溃根因) |
✅ 已修(signal(SIGPIPE, SIG_IGN)) |
| F | val["type"].asInt() 字段缺失/类型不匹配时抛 Json::LogicError 导致 service terminate |
✅ 已修(try-catch 包住 JSON 处理块) |
| G | C_P_array = new Mysql_cli[thread_num] 未值初始化,mysql_con 为野值导致 Reset() 段错误 |
✅ 已修(加 () 值初始化) |
| H | service.h 的 Conf_path/Sql_Conf_path 硬编码到 csproject2/ 目录,service 读错配置文件 |
✅ 已修(改指向 csproject2view/) |
| I | Db_Show_My_Ticket 返回票固定日期 t.DateTime 而非预定时间 y.ctime |
✅ 已修(SQL 改用 y.ctime) |
| J | stop_all.sh 用 pgrep -f 'python3 app.py' / pgrep -x service 全局匹配,误杀 csproject2 的服务 |
✅ 已修(PID 文件方式,只 kill 本项目 PID) |
| K | 前端 loadTickets()/loadMine() 缺 return,loadCurrent().then() 拿到 undefined 抛 TypeError,预定成功却弹"网络错误" |
✅ 已修(加 return 返回 Promise) |
六、改进路线图(按优先级)
| 优先级 | 改进项 | 影响 |
|---|---|---|
| P0 | SQL 注入修复(prepared statement) | 安全 |
| P0 | 密码哈希存储 | 安全 |
| P1 | 连接池加条件变量 | 性能/体验 |
| P1 | signal handler 优雅退出 | 健壮性 |
| P1 | 配置文件路径可配置 | 可移植 |
| P2 | 头文件拆分 | 可维护性 |
| P2 | 全局变量改单例 | 可维护性 |
| P2 | 日志分级 | 运维 |
| P3 | 客户端 recv 超时 | 健壮性 |
| P3 | 管理员后台 | 功能扩展 |
| P3 | send/recv 改 ET 模式 | 性能 |