Software system of takeout platform based on C/S architecture implemented by Qt framework

Posted by xploita on Tue, 11 Jan 2022 14:57:13 +0100

Takeout platform based on C/S architecture

introduction

This time, a takeout platform software with C/S architecture is implemented under the Qt framework. The client uses Qt::Widgets and Qt::Network modules, and the server uses Qt::Sql and Qt::Network modules. The application scenario of the system is: one server instance serves multiple client instances. The client uses GUI interface to support user registration and login; Merchants manage products and process orders; Customers with different preferential levels view products and pay orders; And the system administrator to view the sales log.

Function introduction

  1. User registration and login

  1. Merchants add products

  1. Merchants view and modify products

  1. Merchants view and process orders

  1. Customers view and add shopping carts


  1. Orderers view and settle orders


  1. Changes in the discount level of ordering

  1. The administrator queries the sales log

System analysis and design

  • Overview and class diagram
  1. The behavior control of the client is integrated in the MainWidget class, which is divided into two parts: network communication and GUI form display;
  2. The basic interaction mode with the server is: in the callback of button control click behavior, send the corresponding task id and the information expressed in the current interface to the server, refresh the current interface and jump to the corresponding data display interface after receiving the legal reply from the server;
  3. In terms of GUI, the builder mode is used to build different function menus for different users. The data view of the client is divided into user information view UserForm (user login and registration), product information view ProductForm and order information view OrderForm. All information views maintain all the information of a specific class. When submitting a request, Collect the corresponding information, serialize and send it;
  4. Considering that products and orders have similar display requirements, that is, display all and edit a single, product information and order information are listed in the ViewWidget class of QScrollArea with scroll bar control in the form of sub window lines, and individual editing is carried out by directly selecting and modifying the corresponding sub window lines in the list. In particular, the product list is added by popping up a new product information sub window; For the order list, the addition behavior occurs in the product list. The order list (that is, the user's shopping cart and the merchant's order) only maintains the modification status and submits the modification request.
  5. The server singleton FoodServer processes the connection request through the QTcpServer variable held, and creates a QTcpSocket for each client access;
  6. For each stream data received by QTcpSocket, a ServerNetService is constructed for processing;
  7. ServerNetService reads the task id at the beginning of the stream data to distinguish client requests, obtains the DbHandler corresponding to User, Product and Order through DbHandlerFactory, and performs addition, deletion, query and modification of the database;
  8. The abstract base class of DbHandler encapsulates the database operation based on executing SQLite statement and the corresponding debugging result printing operation provided by QSqlQuery; Execute the parsing task of the database return value through many static methods in the specific DbHandler, mark the parsing result with the task id specified by the communication protocol, and return it to the client through QTcpSocket.

Client class diagram

Server class diagram

Implementation scheme

Four typical design patterns

  • The server
  1. Singleton mode
// realization
class FoodServer : public QObject {
Q_OBJECT
public:
    ~FoodServer();
    static FoodServer &getInstance();
    FoodServer(const FoodServer &) = delete;
    FoodServer &operator=(const FoodServer &) = delete;
private:
    FoodServer();
    QTcpServer *server;
};
// use
auto &server = FoodServer::getInstance();
  • The listening behavior of FoodServer is initialized in the constructor. Through the loop mechanism of QCoreApplication::exec(), insert the instantiation statement in the following code to maintain a global FoodServer instance.
    QCoreApplication a(argc, argv);
    // insert getInstance here
    return a.exec();
  1. Simple factory mode
  • The simple factory mode is applied to the DataBaseHandler. The static method of processing data of the specific DataBaseHandler obtains the database operation behavior corresponding to the specific DataBaseHandler through the simple factory method. Class diagram is as follows:

// use
QString username = ...;
auto db = DbHandlerFactory::getDbHandler("UserTable");
db->queryByString("select", "uname", username);
  1. Strategy mode
  • The policy mode is used for the settlement operation of the user class on the Server. In consideration of security design, the settlement behavior occurs on the Server side. Class diagram is as follows:

// use
double User::pay(double _pay) {
     return discount->calculate(_pay);
}
double pay = user.pay(order.getPrice() * order.getOnum());
order.setOpay(pay);
  • client
  1. Builder pattern
  • The builder mode is used to build the function menu TodoForm. Its responsibility is to display different specific function menus and different user information prompts for different users. Class diagram is as follows:

    // use
    TodoFormBuilder *builder;
    TodoFormController controller;
    switch (user.getUtype()) {
        case seller:
            builder = new SellerTodoBuilder();
            userTodoForm = controller.createUserTodoForm(builder, user);
            ...// connect signals and slots
            break;
        case admin:
            builder = new AdminTodoBuilder();
            userTodoForm = controller.createUserTodoForm(builder, user);
            ...// connect signals and slots
            break;
        case buyer:
        case vip1:
        case vip2:
            builder = new BuyerTodoBuilder();
            userTodoForm = controller.createUserTodoForm(builder, user);
            ...// connect signals and slots
    }
    delete builder;
    

Network Communications

  • The Qt::Network Library encapsulates the operations of sending and receiving tcp messages, but does not provide an interface for receiving all contents completely, which means that a large serialized data stream will be split into multiple tcp packets, triggering the QTcpSocket::readyRead signal many times, resulting in failure to correctly read the serialized data at one time. During the test, it is found that all the data from the client to the server can be sent in one tcp packet, and most of the list data returned by the server (such as all orders and all products) cannot be sent in one tcp packet. Therefore, the server data stream needs to store length information for the client to judge and splice the data in the corresponding tcp packet.
// The server sends TCP packets
QByteArray ServerNetService::process() {
    QDataStream reader(&buffer, QIODevice::ReadOnly);
    reader >> taskid;
    QByteArray result;
    QDataStream writter(&result, QIODevice::WriteOnly);
    qint64 dataSize = 0;
    writter << dataSize;
    switch (TaskId(taskid)) {
        ...                     // Call database operation
    }
    dataSize = result.size();   // Write buffer length to buffer header
    QDataStream _writter(&result, QIODevice::WriteOnly);
    _writter << dataSize;
    return result;
}
connect(socket, &QTcpSocket::readyRead, [=]() {
    // The data of the client can be read at one time
    ServerNetService netio(socket->readAll());
    socket->write(netio.process());
});
// When the client receives TCP packets, it needs to consider the problem of multiple reads in splicing
connect(socket, &QTcpSocket::readyRead, [=]() {
    QDataStream preReader(&toAppendBuffer, QIODevice::ReadOnly);
    auto tmp = socket->readAll();
    if (toAppendBuffer.isEmpty()) { // Read the first package
        toAppendBuffer.append(tmp);
        preReader >> toReceiveBufferSize;
        if (toAppendBuffer.size() < toReceiveBufferSize) 
            return;
    } else {                        // Read intermediate tcp packets
        toAppendBuffer.append(tmp);
        if (toAppendBuffer.size() < toReceiveBufferSize) 
            return;
    }// Read a complete data stream for further processing
    QDataStream reader(&toAppendBuffer, QIODevice::ReadOnly);
    reader >> toReceiveBufferSize;
    int taskid;
    reader >> taskid;
    process(TaskId(taskid), reader);
    toAppendBuffer.clear();
});

Database operation

  • Take the product list as an example:
class ProductDbHandler : public DataBaseHandler {
public:
    ProductDbHandler() { setTableName("ProductTable"); }
    static inline QList<Product> queryProducts(QSqlQuery &q);
    static void db_addProduct(const Product &product, bool putId = false);
    static void db_delProduct(const int pid);
    static QList<Product> db_getProduct();//All products
    static QList<Product> db_getProduct(const int pid);//Product with specified number
    static QList<Product> db_getProduct4Seller(const int uid);//All products of a merchant
};
class DbHandlerFactory {
public:
    static std::shared_ptr<DataBaseHandler> getDbHandler(const QString &tableName, bool logging = false);
};
// Take an example of a static member function
void ProductDbHandler::db_delProduct(const int pid) {
    auto db = DbHandlerFactory::getDbHandler("ProductTable");
    db->queryByInt("delete", "pid", pid);
    db->exec();
}
  • The reason why we want to write the database operation method in an object-oriented way, such as static member function, is because the return values of query orders and product lists are qlist < < specific class >, and there is no pointer or intelligent pointer to instantiate the template, which is inconvenient to design a unified image to process the return value. Here, only the basic database statement execution methods are abstracted to simplify the code.

serialization and deserialization

  • QDataStream class supports serialization and deserialization of User-defined data through overloading > > and < < operators. For list data, QDataStream class supports serialization of QList type, and the class must have specific implementations of > > and <. Take the User class as an example:
QDataStream &operator<<(QDataStream &s, const User &user) {
    s << user.uid << user.utype << user.uname << user.password << user.photo;
    return s;
}
QDataStream &operator>>(QDataStream &s, User &user) {
    s >> user.uid >> user.utype >> user.uname >> user.password >> user.photo;
    return s;
}

GUI control

  • GUI control relies on Qt signal slot mechanism, which is similar to callback function. The principle is not described here. Summarize three scenarios that trigger window changes:
  1. User edit text box, edit check box, drop-down menu
  2. The user clicks the button
  3. Client receives server response
  • The user's behavior of editing the input control is managed by the background of Qt framework, which only needs to deal with the synchronous change relationship between the quantity and total price of similar products; The response of button clicking includes two follow-up actions: pulling up QMessageBox prompt and submitting network request; Receiving the server response means that the request sent by the button is replied. The reply will be deserialized and the data will be presented to a new display window.

Sales log

  • Query criteria directly:
QList<Order> OrderDbHandler::db_getOrderDone() {
    auto db = DbHandlerFactory::getDbHandler("OrderTable");
    db->queryByInt("select", "ostate", Ostate::done);// Query completed orders
    return OrderDbHandler::queryOrders(db->exec());
}
  • Then count with a hash table
QHash<QString, double> OrderDbHandler::getCountByMonth(const QList<Order> &orders) {
    QHash<QString, double> count;
    for (auto &r:orders) {  // Date example: Saturday June 20 09:31:45 2020
        auto split = r.getSubmittime().split(" ");
        if (count.find(split[1]) == count.end()) {
            count[split[1]] = r.getOpay();
        } else {
            count[split[1]] += r.getOpay();
        }
    }
    return count;
}

System compliance with design principles

  • The server separates the network communication class and database class, which conforms to the principle of single responsibility. The client integrates the network communication and GUI interaction, which does not conform to the principle of single responsibility
  • The design of Order class and OrderWithFullInfo class conforms to the Richter substitution principle. OrderWithFullInfo class only adds some member variables and functions on the basis of Order class, and the inheritance relationship does not destroy the original functions of Order class
  • The FoodServer class holds a reference to QTcpServer instead of inheriting and overwriting related methods, which conforms to the principle of composition / aggregation reuse

Summary

The takeout platform software implemented in this operation has C/S architecture and supports the interactive scenario of multiple users logging in at the same time; The GUI interface is realized; Single instance mode, simple factory mode, strategy mode and builder mode are used; It fully considers various possibilities in the interaction process and has good robustness

Topics: C Qt architecture