预约系统版本2(pyhton+flask可视化版本)

预约系统版本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_Poolpthread_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 启动时绑死一个连接(线程局部存储 __threadpthread_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_RegisterDb_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+Cepoll_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 == 0delete 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_ConfigSocketAccSocketRecvSocketThread_PoolMysql_configMysql_ClientMysql_cliConnect_PoolTcp_Service 10 个类型全堆在一个文件。

改进 :拆分成 socket.hthread_pool.hmysql_pool.hmysql_client.htcp_service.hservice_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* taskMysql_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 8888MAXLINE 256d_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_lockwhile(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 个业务分支
  • 结束后 epoll_ctl MOD 重新监听(EPOLLONESHOT)
Mysql_Client::Connect_Mysql_Server (L474-L499)

加锁遍历 C_P_array,找到 status==true 的 slot → mysql_con = C_P_array[i].mysql_constatus=falseuser++ → 返回 true。借不到返回 false。

Mysql_Client::~Mysql_Client (L740-L749)

加锁遍历 C_P_array,找到 mysql_con 相等的 slot → status=trueuser-- 归还。

Connect_Pool::Run (L776-L798)

循环 5 次:mysql_initmysql_real_connect → 存进 C_P_array[i].mysql_con + status=true

Connect_Pool::Reset (L801-L809)

循环 5 次:若 mysql_con != nullptrmysql_close,然后置 nullptr + status=false。

业务方法
  • Db_User_Register (L501) --- 插入 user_info
  • Db_User_Login (L517) --- select Name,Passwd 比对
  • Db_Show_Ticket (L552) --- select * 拼成 JSON
  • Db_Yd_Ticket (L585) --- 事务:查 max/count → 更新 count → 插 yd_table
  • Db_Show_My_Ticket (L647) --- JOIN yd_table + ticket_table
  • Db_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-time vs day_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 + connect
  • Show_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(用了 autonullptr
  • 拆分头文件后改用 .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:两个表单,通过 .active class 切换显示
  • #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:两个视图容器,通过 .active class 切换
  • #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 切换用 .active class
  • 主页: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 ({
        "&": "&amp;", "<": "&lt;", ">": "&gt;",
        '"': "&quot;", "'": "&#39;"
      })[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 后 KILLkill 优雅停止,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_Serveri 不递归导致死循环 ✅ 已修
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.hConf_path/Sql_Conf_path 硬编码到 csproject2/ 目录,service 读错配置文件 ✅ 已修(改指向 csproject2view/
I Db_Show_My_Ticket 返回票固定日期 t.DateTime 而非预定时间 y.ctime ✅ 已修(SQL 改用 y.ctime
J stop_all.shpgrep -f 'python3 app.py' / pgrep -x service 全局匹配,误杀 csproject2 的服务 ✅ 已修(PID 文件方式,只 kill 本项目 PID)
K 前端 loadTickets()/loadMine()returnloadCurrent().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 模式 性能
相关推荐
__zRainy__1 小时前
Node系列 · ORM:mysql 驱动程序
数据库·后端·mysql·node.js·orm
程序员夏洛1 小时前
MySQL 中如果发生死锁应该如何解决?
数据库·mysql
肠畔码农1 小时前
Redis 深度内核解析与高性能运维调优指南
运维·数据库·redis
爱奥尼欧1 小时前
10.C++ string 实现详解
java·开发语言·c++
小飞学编程...1 小时前
【equals 、Comparable、Comparator 三者的区别】
java·python
钱六两1 小时前
SpringAI集成RAG实操(postgresql+pgvector)中(集成到项目中)
数据库·postgresql
青 春 记 忆1 小时前
零基础入门python21:Flask账本的数据模型——用户、分类和账目
python·flask·后端开发
hy.z_7771 小时前
【软件测试】8. 自动化测试实践
python
蒲锘1 小时前
DevOps 实验项目笔记三 —— Kubernetes 集群部署阶段
笔记·kubernetes·devops·rbac