【项目】基于Linux和C++的动态在线视频点播系统设计

1. 前言

1.1 源码

源码链接:

1.2 项目简介

  • 搭建视频点播服务器,根据不同用户对服务器的访问,实现视频的增删查改,可以根据类型进行查看播放的功能。

  • 用户管理

  • 两种用户,普通用户可以上传视频,修改 / 删除 自己上传 的视频;管理员用户 可以对服务器的所有视频进行增删查改;


1.3 实现内容

完成服务器端的程序业务功能的实现 以及前端访问界面 html 的编写,能够⽀持客户端中不同用户在浏览器针对服务器上的视频进行操作;


1.4 涉及技术 / 环境

  • 编程语言:C++
  • 操作系统:Linux unbuntu20.4
  • 数据库:MariaDB
  • 序列化 / 反序列胡:jsoncpp
  • 网络库:httplib
  • 网络协议: http
  • 前端:html/css/javascript AJAX

2. 整体架构

2.1 服务器功能

  • 针对客户端上传的视频文件以及封面图片进行备份存储。
  • 针对客户端上传的视频完成增删改查功能
  • ⽀持客户端浏览器进行视频的观看功能
  • 用户的基本管理(注册、登录、修改)
  • 根据不同用户对视频的管理权限不同

2.2 服务器结构

  • 用户处理模块:处理用户相关的操作。
  • 数据管理模块:负责针对客⼾端上传的视频信息进行管理。
  • 网络通信模块:搭建⽹络通信服务器,实现与客户端通信。
  • 业务处理模块:针对客⼾端的各个请求进⾏对应业务处理并响应结果。
  • 前端界面模块:完成前端浏览器上视频共享点播的各个 html 页面,在页面中⽀持增删改查以及观看功能,以及用户的相关操作

3. 前提步骤

3.1 思路分析

如何存储视频信息以及如何将视频发送给客户端?

显然这是首要的问题,对于视频信息的存储,这里利用 数据库 + 文件 的方法,即:

  • 数据库存储每个视频的相关信息,比如名称、简介、视频路径、封面路径等;对于视频的本体内容,数据库中存储的是其存储路径;
  • 根据后文实现的工具类,我们通过二进制方式将视频文件写入到一个新文件中,当客户端要获取内容时,同理将视频文件提取出来。

3.2 创建视频表

我们可以根据下面的语句 创建视频表(其他属性可以根据需求添加,比如视频时常等):
在这里插入图片描述

表结构如下:
在这里插入图片描述


4. 后端 基本功能实现(视频点播)

4.1 服务端工具类实现

工具类主要实现多个Util类,代码如下:

namespace aod
{
    class JsonUtil
    {
    public:
        static bool Serialize(const Json::Value& jv, std::string& body) 
        {} // 将json对象序列化成字符串

        static bool Deserialize(const std::string& body, Json::Value& jv)
        {} // 将字符串反序列化成json对象
    };

    // 文件功能类
    class FileUtil
    {
    private:
        std::string _file_name;
    public:
        FileUtil(std::string file_name) : _file_name(file_name) {}
        bool Exists() {} // 文件是否存在

        size_t FileSize() {} // 获取文件大小

        bool CreateDirectory() {} // 创建文件夹

        bool GetContent(std::string& content) {} // 将文件读取到content中
    
        bool SetContent(const std::string& content) {}; // 将Content设置到文件中
   };
}

4.2 日志输出类

为了方便在编码阶段进行功能测试,可以编写一个日志辅助类:Logger.hpp

#ifndef __M_LOG_H__
#define __M_LOG_H__

#include <iostream>
#include <cstdio>
#include <ctime>

#define DBG_LEVEL 0
#define INF_LEVEL 1
#define ERR_LEVEL 2
#define DEFAULT_LEVEL DBG_LEVEL

#define LOG(level_str, level, format, ...) \
    if (level >= DEFAULT_LEVEL) { \
        time_t t = time(nullptr); \
        struct tm* ptm = localtime(&t); \
        char time_str[32]; \
        strftime(time_str, /*sizeof(time_str)*/31, "%H:%M:%S", ptm); \
        printf("[%s][%s][%s:%d]\t" format "\n", level_str, time_str, __FILE__, __LINE__, ##__VA_ARGS__); \
    } \

#define DLOG(format, ...) LOG("DBG", DBG_LEVEL, format, ##__VA_ARGS__)
#define ILOG(format, ...) LOG("INF", INF_LEVEL, format, ##__VA_ARGS__)
#define ELOG(format, ...) LOG("ERR", ERR_LEVEL, format, ##__VA_ARGS__)

#endif

4.3 数据库/表 管理类

对于这一部分,主要实现两个类,即SqlManagerTableVideo类,分别用于进行数据库的相关操作以及视频表的相关操作:

  • SqlManager
    • MysqlInit() - 初始化Mysql句柄
    • MysqlDestory() - 关闭mysql
    • MysqlQuery() - 执行mysql语句
  • TableVideo
    • Insert() - 插入指定视频
    • Delete() - 删除指定视频
    • Update() - 更新视频信息
    • SelectAll - 查找所有视频
    • SelectById() - 查找单个指定视频
    • SelectByType() - 根据类型查找视频
    • SelectLikes() - 查找关键词相关视频
#ifndef SQL_MANAGER_HPP
#define SQL_MANAGER_HPP

#include <iostream>
#include <mariadb/mysql.h>
#include <mutex>
#include <jsoncpp/json/json.h>
#include "../common/Logger.hpp"

namespace aod
{
    #define HOSTNAME "localhost"
    #define USERNAME "user"
    #define PASSWORD "123456"
    #define DATABASE "aod_system"

    class SqlManager
    {
    public:
        static MYSQL* MysqlInit() // 初始化mysql句柄
        {
            MYSQL* mysql = mysql_init(nullptr);
            if (mysql == nullptr)
            {
                DLOG("mysql_init failed: %s", mysql_error(mysql));
                return nullptr;
            }

            if(mysql_real_connect(mysql, HOSTNAME, USERNAME, PASSWORD, DATABASE, 0, nullptr, 0) == nullptr) {
                DLOG("mysql_real_connect failed: %s", mysql_error(mysql));
                mysql_close(mysql);
                return nullptr;
            }

            return mysql;
        }

        static bool MysqlDestroy(MYSQL* mysql)
        {
            if(mysql == nullptr) {
                DLOG("mysql is nullptr when destroy");
                return true;
            }

            mysql_close(mysql);
            return true;
        }

        static bool MysqlQuery(MYSQL* mysql, const std::string& sql)
        {
            if(mysql_query(mysql, sql.c_str()) != 0) {
                DLOG("mysql_query failed: %s | %s", sql.c_str(), mysql_error(mysql));
                return false;
            }
            return true;
        }
    };

    class TableVideo
    {
    private:
        MYSQL* _mysql;
        std::mutex _mutex;
    public:
        TableVideo() {
            _mysql = SqlManager::MysqlInit();
            if(_mysql == nullptr) {
                DLOG("TableVideo init failed");
                exit(-1);
            }
        }

        ~TableVideo() {
            SqlManager::MysqlDestroy(_mysql);
        }

        bool Insert(const Json::Value& video) {} // 插入新的视频

        bool Delete(const int& video_id) {} // 删除视频
	
		// 更新视频信息
        bool Update(int video_id, const Json::Value& video) {} // 更新视频信息

        bool SelectAll(Json::Value* videos) {} // 选择全部视频

        bool SelectById(const int& video_id, Json::Value* video) {} // 根据id选择视频 / 选择一个指定视频

        bool SelectByType(const std::string video_type, Json::Value* videos) {} // 根据视频类型选择

        bool SelectLikes(const std::string& key, Json::Value* videos) {} // 模糊匹配,查找输入的关键词
    };
}

#endif // SQLMANAGER_H

对于TableVideo,即具体的表操作,这里分别以InsertSelectAll举例:

对视频的相关的增删查改,首先先定义一个sql语句,再调用SqlManagerSqlQuery执行即可,这里我们利用resize、#define直接定义,与sprintf搭配将内容输出到sql语句。

bool Insert(const Json::Value& video) {
    if(video["name"].asString().empty()) {
        DLOG("video name is empty when insert");
        return false;
    }
    
    std::string sql;
    sql.resize(video["info"].asString().size() + 4096);
    #define INSERT_VIDEO "INSERT INTO tb_video VALUES (NULL, '%s', '%s', '%s', '%s', '%s');"
    sprintf(&sql[0], INSERT_VIDEO, video["name"].asCString(), 
                                   video["info"].asCString(), 
                                   video["video"].asCString(), 
                                   video["image"].asCString(), 
                                   video["type"].asCString());

    return SqlManager::MysqlQuery(_mysql, sql.c_str());
} // 插入新的视频

对于查找,首先通过MysqlQuery执行查询语句:

  • 后通过 MYSQL_RES* res = mysql_store_result(_mysql); 获取结果
  • 再利用size_t num_rows = mysql_num_rows(res); 获取结果行数
  • 循环 进行 MYSQL_ROW row = mysql_fetch_row(res); 获取每行的内容,将内容加载到Json::Value的变量video中,最后上层拿到输出型参数videos
bool SelectAll(Json::Value* videos) {
            #define SELECT_ALL "SELECT * FROM tb_video;"
            _mutex.lock(); // 保护 查询本地数据的操作过程
            if(!SqlManager::MysqlQuery(_mysql, SELECT_ALL)) {
                DLOG("mysql_query failed: %s | %s", SELECT_ALL, mysql_error(_mysql));
                _mutex.unlock();
                return false;
            }

            MYSQL_RES* res = mysql_store_result(_mysql);
            if(res == nullptr) {
                DLOG("mysql_store_result failed: %s", mysql_error(_mysql));
                _mutex.unlock();
                return false;
            }

            _mutex.unlock();

            size_t num_rows = mysql_num_rows(res);
            if(num_rows == 0) {
                DLOG("mysql_num_rows failed: %s", mysql_error(_mysql));
                mysql_free_result(res);
                return true;
            }

            for(int i = 0; i < num_rows; ++i) {
                MYSQL_ROW row = mysql_fetch_row(res);
                if(row == nullptr) {
                    DLOG("mysql_fetch_row failed: %s", mysql_error(_mysql));
                    mysql_free_result(res);
                    return false;
                }

                Json::Value video;
                video["id"] = atoi(row[0]);
                video["name"] = row[1];
                video["info"] = row[2];
                video["video"] = row[3];
                video["image"] = row[4];
                video["type"] = row[5];

                videos->append(video);  
            }

            mysql_free_result(res);
            return true;
        }

4.4 服务器

对于服务器,首先是下面的代码,保留了每个成员函数的声明,其主要功能是:

通过RunModel 启动服务器,对于该函数:

  1. 初始化数据库、创建视频存储相关路径
  2. 设置静态资源根目录(方便客户端获取资源)
  3. 添加 [请求-响应] 的映射关系: _server.Post("/video", Insert);
  4. 启动服务,监听内容;
#include <iostream>
#include "httplib.h"
#include "../common/Logger.hpp"
#include "../common/Util.hpp"
#include "../common/sqlManager.hpp"
#include "../common/userManager.hpp"

namespace aod {
#define WWWROOT "./wwwroot"
#define VIDEOROOT "/video/"
#define IMAGEROOT "/image/"

    TableVideo* _tb_video = nullptr;
    TableUser* _tb_user = nullptr;

    class Server {
    private:
        uint16_t _port;
        httplib::Server _server;

        // 设置错误响应
        static void set_error_response(httplib::Response& resp, int code, const std::string& reason);

    public:
        Server(uint16_t port) : _port(port) {}

        // 启动服务器并监听指定端口
        bool RunModel();

    private:
        // 注册用户处理函数
        static bool Register(const httplib::Request& req, httplib::Response& resp);

        // 用户登录处理函数
        static bool Login(const httplib::Request& req, httplib::Response& resp);

        // 用户登出处理函数
        static bool Logout(const httplib::Request& req, httplib::Response& resp);

        // 获取用户信息处理函数
        static void GetUserInfo(const httplib::Request& req, httplib::Response& resp);

        // 插入视频信息处理函数
        static void Insert(const httplib::Request& req, httplib::Response& resp);

        // 删除视频信息处理函数
        static void Delete(const httplib::Request& req, httplib::Response& resp);

        // 更新视频信息处理函数
        static bool Update(const httplib::Request& req, httplib::Response& resp);

        // 根据视频ID查询视频信息处理函数
        static void SelectById(const httplib::Request& req, httplib::Response& resp);

        // 查询所有视频信息处理函数
        static void SelectAll(const httplib::Request& req, httplib::Response& resp);

        // 根据视频类型查询视频信息处理函数
        static void SelectByType(const httplib::Request& req, httplib::Response& resp);
    };
}
bool RunModel()
{
    // 1. 初始化数据库
    _tb_video = new TableVideo();

    std::string wwwroot_path = WWWROOT;
    FileUtil(wwwroot_path).CreateDirectory();
    std::string video_path = wwwroot_path + VIDEOROOT;
    FileUtil(video_path).CreateDirectory();
    std::string image_path = wwwroot_path + IMAGEROOT;
    FileUtil(image_path).CreateDirectory();
    // 1.5 设置静态资源根目录
    _server.set_mount_point("/", wwwroot_path);
    // 2. 添加 请求-处理函数 映射关系
    _server.Post("/video", Insert);
    _server.Put("/video/(\\d+)", Update);
    _server.Delete("/video/(\\d+)", Delete);
    _server.Get("/video", SelectAll);
    _server.Get("/video/(\\d+)", SelectById);
    _server.Get("/video/type", SelectByType);
    _server.Post("/login", Login);
    _server.Post("/register", Register);
    _server.Get("/logout", Logout);

    // 3. 启动服务
    bool ret = _server.listen("0.0.0.0", _port);
    if (ret == false)
    {
        ELOG("server start failed");
        return false;
    }

    return true;
}

5. 功能测试

完成上述功能后,对于视频点播的后端部分其实已经基本完毕,此时可以进行测试,这里使用Apifox工具进行测试:

这里以获取视频信息为例,当设置环境为当前服务器主机后,直接发送Get请求,可以正确获取到内容,对于其他功能,均测试成功;
在这里插入图片描述


6. 前端部分

前端部分这里首先对一个简单模板进行修改,得到一个css修饰后的html界面。对于html、css部分,这里不再概述(源码中看),重点在于前后端交互,是如何获取后端响应并显示到屏幕中的:

6.1 部分功能实现

① 视频播放 | 显示

对于主页index.html的vue.js,我们定义一个get_all_videos的方法,发送请求到客户端,将结果获取到videos数组中,最后调用该函数

<script>
	let app = new Vue({
		el: '#myapp',
		data: {
			author: "wqy",
			videos: []
		},
		methods: {
			get_all_videos: function () {
				$.ajax({
					url: "/video",
					type: "get",
					context: this,
					success: function (result, status, xhr) {
						this.videos = result;
					}
				})
			}
        }
	});
	app.get_allvideos();
</script>

如何获取视频展示到界面中?在get_all_videos中已经获取了所有的视频信息,此时在html中利用v-for遍历所有视频,并根据其内容进行展示:

在这里插入图片描述

<div class="row auto-clear">
<article class="col-lg-3 col-md-6 col-sm-4" v-for="video in videos">
	<!-- POST L size -->
	<div class="post post-medium">
		<div class="thumbr">
			<a class="afterglow post-thumb" v-bind:href="'/video.html?id='+video.id"
				target="_blank">
				<span class="play-btn-border" title="Play"><i
						class="fa fa-play-circle headline-round"
						aria-hidden="true"></i></span>
				<div class="cactus-note ct-time font-size-1"><span>02:02</span>
				</div>
				<img class="img-responsive" v-bind:src="video.image" alt="#"
					v-cloak>
			</a>
		</div>
		<div class="infor" style="display: flex; flex-direction: column;">
			<h4>
				<a class="title" href="#" v-cloak>{{video.name}}</a>
			</h4>
			<span class="posts-txt" title="Posts from Channel" style="display: flex; align-items: center; justify-content: space-between;">
				<div style="display: flex; align-items: center;">
					<i class="fa fa-thumbs-up" aria-hidden="true"></i>
					<span id="random-number"></span>
				</div>
				<div style="display: flex; align-items: center; margin-left: auto;">
					<img src="img/video-type.png" alt="视频类型图标" style="width: 16px; height: 16px; vertical-align: middle;">
					<span class="video-type">{{video.type}}</span>
				</div>
			</span>
		</div>
	</div>
</article>
</div>

当我们点击视频图标时,会创建并跳转到一个新的网页用于播放视频:

<a class="afterglow post-thumb" v-bind:href="'/video.html?id='+video.id" target="_blank">
    <span class="play-btn-border" title="Play">
        <i class="fa fa-play-circle headline-round" aria-hidden="true"></i>
    </span>
    <img class="img-responsive" v-bind:src="video.image" alt="#" v-cloak>
</a>

② 搜索功能

对于搜索模块,当输入内容后点击搜索,会转入到新网页用于展示搜索到的内容:

<div class="search-block">
	<form method="GET" action="search_results.html">
		<input type="search" name="search" placeholder="Search" required> <!-- 添加name属性 -->
		<button type="submit" style="">Search</button> <!-- 添加提交按钮 -->
	</form>
</div>

对于search.html, 各种控件与index.html没有差别,主要是js部分:

由于后端在SelectAll中有两种情况,即返回全部结果,或者根据给出的关键词进行匹配搜索,对于search.html,自然进行后者,所以在发送AJAX请求时,应该传递一个search参数,后获取服务器返回的结果;

<script>
    let searchApp = new Vue({
        el: '#searchApp',
        data: {
            searchResults: []
        },
        created() {
            // 获取 URL 中的搜索参数
            let urlParams = new URLSearchParams(window.location.search);
            let searchQuery = urlParams.get('search');

            // 发起 AJAX 请求
            $.ajax({
                url: '/video',  // 发向你的后端处理函数
                type: 'get',
                data: { search: searchQuery },  // 传递 search 参数
                context: this,
                success: function (result) {
                    this.searchResults = result;  // 将结果存储并渲染
                }
            });
        }
    });
</script>

6.2 根据视频类型搜索

在这里插入图片描述

要实现具体的功能,我们编写下面的js代码:

  1. 数据categories 包含视频分类,selectedCategory 存储当前选中的分类,videosfilteredVideos 用于存储视频数据和过滤后的结果。

  2. 生命周期钩子created 方法在实例创建时调用,触发 fetchVideos 方法从服务器获取视频数据。

  3. 方法

    • fetchVideos 使用 AJAX 请求获取视频列表,并在成功后调用 filterVideos 进行初步过滤。
    • filterByCategory 方法根据用户选择的分类更新 selectedCategory,并调用 filterVideos 进行过滤。
    • filterVideos 根据 selectedCategory 更新 filteredVideos,如果选中“全部”,则显示所有视频,否则只显示匹配的分类视频。
<script>
    let categoryApp = new Vue({
        el: '#categoryApp',
        data: {
            categories: ['全部', '美食', '教育', '娱乐', '科技', '游戏', '动画', '舞蹈'],
            selectedCategory: '全部',
            videos: [],
            filteredVideos: []
        },
        created() {
            this.fetchVideos();
        },
        methods: {
            fetchVideos() {
                $.ajax({
                    url: '/video',
                    type: 'get',
                    context: this,
                    success: function (result) {
                        this.videos = result;
                        this.filterVideos();
                    }
                });
            },
            filterByCategory(category) {
                this.selectedCategory = category;
                this.filterVideos();
            },
            filterVideos() {
                if (this.selectedCategory === '全部') {
                    this.filteredVideos = this.videos;
                } else {
                    this.filteredVideos = this.videos.filter(video => video.type === this.selectedCategory);
                }
            }
        }
    });
</script>

在总体的显示界面,通过遍历filterVideos,获取不同类别的内容并显示:

<section id="video-main">
    <h2 class="icon"><i class="fa fa-video-camera" aria-hidden="true"></i> {{ selectedCategory }}
        类别的视频</h2>
    <div class="row">
        <div class="col-lg-9 col-md-12 col-sm-12">
            <div class="row auto-clear">
                <div v-if="filteredVideos.length === 0" class="no-results">
                    <h3>没有找到相关内容</h3>
                </div>
                <article v-else class="col-lg-3 col-md-6 col-sm-4" v-for="video in filteredVideos"
                    :key="video.id">
                    <div class="post post-medium">
                        <div class="thumbr">
                            <a class="afterglow post-thumb" :href="'/video.html?id='+video.id"
                                target="_blank">
                                <span class="play-btn-border" title="Play">
                                    <i class="fa fa-play-circle headline-round"
                                        aria-hidden="true"></i>
                                </span>
                                <div class="cactus-note ct-time font-size-1">
                                    <span>{{ video.duration }}</span>
                                </div>
                                <img class="img-responsive" :src="video.image" alt="#">
                            </a>
                        </div>
                        <div class="infor" style="display: flex; flex-direction: column;">
                            <h4>
                                <a class="title" href="#" v-cloak>{{ video.name }}</a>
                            </h4>
                            <span class="posts-txt"
                                style="display: flex; align-items: center; justify-content: space-between;">
                                <div style="display: flex; align-items: center;">
                                    <i class="fa fa-thumbs-up" aria-hidden="true"></i>
                                    <span>{{ video.likes }}</span>
                                </div>
                                <div style="display: flex; align-items: center; margin-left: auto;">
                                    <img src="img/video-type.png" alt="视频类型图标"
                                        style="width: 16px; height: 16px;">
                                    <span class="video-type">{{ video.type }}</span>
                                </div>
                            </span>
                        </div>
                    </div>
                </article>
            </div>
        </div>
        <div class="col-lg-3 hidden-md col-sm-12 text-center top-sidebar"></div>
    </div>
</section>

7. 功能扩充

用户管理类

我们根据之前对视频表的管理类同理编写一个用户管理类,下面是整体类的框架(基本函数的声明)

对于用户表,一般都应该有一个不可重复项,用于底层代码管理用户,可以是id、name、email等等;这里将email设为不可重复项,并依此管理用户:

class TableUser
    {
    private:
        std::mutex _mutex;
        MYSQL *_mysql;

    public:
        TableUser()
        {
            _mysql = SqlManager::MysqlInit();
            if (_mysql == nullptr)
            {
                DLOG("mysql init failed");
                exit(-1);
            }
        }

        ~TableUser()
        {
            SqlManager::MysqlDestroy(_mysql);
        }

    public:
        bool DeleteSessionID(const std::string &sessionID) {}
		// 删除会话id
		
        bool GetUserInfoBySessionID(const std::string &sessionID, Json::Value &user) {}
        // 根据会话id获取用户信息

        bool InsertSessionID(const std::string &email, const std::string &sessionID) {}
        // 加入某个用户的会话id

        bool UserInfoMatching(const Json::Value &user) {}
		// 判断用户信息是否匹配(登陆时用)
		
        bool isUserExist(const std::string &email) {} // 判断是否已经存在该用户(email不允许重复)

        bool Insert(const Json::Value &user)
        {}
        // 将用户加入到用户表

        bool Delete(int userId) {}
		// 删除用户
		
        bool Update(const int& id, const Json::Value &user) {}
        // 更新指定用户信息
    };

对于该用户管理类的成员函数,下面是具体实现:

bool DeleteSessionID(const std::string &sessionID) {
            if(sessionID.empty()) {
                DLOG("Invalid sessionID when delete sessionID");
                return false;
            }

            #define DELETE_SESSION "UPDATE tb_user SET sessionID = '' WHERE sessionID = '%s';"
            std::string sql;
            sql.resize(256);
            sprintf(&sql[0], DELETE_SESSION, sessionID.c_str());

            return SqlManager::MysqlQuery(_mysql, sql.c_str());
        } // 删除sessionID

        bool GetUserInfoBySessionID(const std::string &sessionID, Json::Value &user) {
            if(sessionID.empty()) {
                DLOG("Invalid sessionID when get user info");
                return false;
            }

            #define GET_USER_INFO_BY_SESSION "SELECT * FROM tb_user WHERE sessionID = '%s';"
            std::string sql;
            sql.resize(256);
            
            sprintf(&sql[0], GET_USER_INFO_BY_SESSION, sessionID.c_str());

            if(SqlManager::MysqlQuery(_mysql, sql.c_str())== false) {
                DLOG("执行语句失败");
                return false;
            }

            MYSQL_RES *result = mysql_store_result(_mysql);
            if(result == nullptr) {
                DLOG("获取结果失败");
                return false;
            }

            MYSQL_ROW row = mysql_fetch_row(result);
            if(row == nullptr) {
                DLOG("获取结果为空");
                return false;
            }

            user["id"] = std::stoi(row[0]) ? std::stoi(row[0]) : -1;
            user["identity"] = row[1] ? row[1] : "未知";
            user["name"] = row[2] ? row[2] : "未知";
            user["email"] = row[3] ? row[3] : "未知";
            user["password"] = row[4] ? row[4] : "未知";
            user["sessionID"] = row[5] ? row[5] : "未知";

            mysql_free_result(result);
            return true;
        } // 通过sessionID获取用户信息

        bool InsertSessionID(const std::string &email, const std::string &sessionID) {
            if (email.empty() || sessionID.empty()) {
                DLOG("Invalid email or sessionID when insert sessionID");
                return false;
            }

            #define INSERT_SESSION "UPDATE tb_user SET sessionID = '%s' WHERE email = '%s';"
            std::string sql;
            sql.resize(256);
            sprintf(&sql[0], INSERT_SESSION, sessionID.c_str(), email.c_str());

            return SqlManager::MysqlQuery(_mysql, sql.c_str());
        } // 加入会话id

        bool UserInfoMatching(const Json::Value &user) {
            std::string identity = user["identity"].asString();
            std::string name = user["name"].asString();
            std::string email = user["email"].asString();
            std::string password = user["password"].asString();
            
            if(identity.empty() || name.empty() || email.empty() || password.empty()) {
                DLOG("Invalid user info when check matching");
                return false;
            }

            #define USER_INFO_MATCHING "SELECT * FROM tb_user WHERE identity = '%s' AND name = '%s' AND email = '%s' AND password = '%s';"
            std::string sql;
            sql.resize(256);
            sprintf(&sql[0], USER_INFO_MATCHING, identity.c_str(), name.c_str(), email.c_str(), password.c_str());

            return SqlManager::MysqlQuery(_mysql, sql.c_str());
        } // 判断用户信息是否匹配

        bool isUserExist(const std::string &email) {
            if (email.empty()) {
                DLOG("Invalid email when check user exist");
                return false;
            }   
            
            std::string sql;
            sql.resize(128);

            #define CHECK_USER "SELECT * FROM tb_user WHERE email = '%s';"
            sprintf(&sql[0], CHECK_USER, email.c_str());

            return SqlManager::MysqlQuery(_mysql, sql.c_str());
        } // 判断是否已经存在该用户(email不允许重复)

        bool Insert(const Json::Value &user)
        {
            if (user["name"].asString().empty() || user["email"].asString().empty() || user["password"].asString().empty())
            {
                DLOG("User name, email or password is empty when insert");
                return false;
            }

            std::string sql;
            sql.resize(1024);

            #define INSERT_USER "INSERT INTO tb_user (identity, name, email, password) VALUES ('%s', '%s', '%s', '%s');"
            sprintf(&sql[0], INSERT_USER, user["identity"].asCString(),
                    user["name"].asCString(),
                    user["email"].asCString(),
                    user["password"].asCString());

            return SqlManager::MysqlQuery(_mysql, sql.c_str());
        } // 插入用户到表中

        bool Delete(int userId) {
            if (userId <= 0) {
                DLOG("Invalid user ID when delete");
                return false;
            }
            
            std::string sql;
            sql.resize(128);
            #define DELETE_USER "DELETE FROM tb_user WHERE id = %d;"
            sprintf(&sql[0], DELETE_USER, userId);

            return SqlManager::MysqlQuery(_mysql, sql.c_str());
        } // 从表中删除指定用户

        bool Update(const int& id, const Json::Value &user) {
            if(user["name"].asString().empty()) {
                DLOG("User name is empty when update");
                return false;
            }

            std::string sql;
            sql.resize(1024);
            #define UPDATE_USER "UPDATE tb_user SET name = '%s', email = '%s', password = '%s' WHERE id = %d;"
            sprintf(&sql[0], UPDATE_USER, user["name"].asCString(), user["email"].asCString(), user["password"].asCString(), id);

            return SqlManager::MysqlQuery(_mysql, sql.c_str());
        } // 更新指定用户信息

再完成了用户管理类的基本操作后,此时可以编写服务器对客户端发来的用户相关请求的处理函数:

首先在Util.hpp 中 实现一个功能:生成会话id(用于登录用户后保存会话id)

        static std::string generate_session_id()
        {
            std::stringstream ss;
            std::random_device rd;
            std::mt19937 mt(rd());
            std::uniform_int_distribution<int> dist(0, 15);
            for (int i = 0; i < 32; ++i)
            {
                int r = dist(mt);
                ss << (r < 10 ? char('0' + r) : char('a' + r - 10));
            }
            return ss.str();
        } // 生成32位随机session_id

下面是服务器的处理函数:

 // 注册
static bool Register(const httplib::Request &req, httplib::Response &resp) {}
 // 登录用户
static bool Login(const httplib::Request &req, httplib::Response &resp) {}
 // 登出用户
static bool Logout(const httplib::Request &req, httplib::Response &resp) {}
 // 注销用户
static bool DeleteUser(const httplib::Request &req, httplib::Response &resp) {}
 // 获取当前登录用户信息
static void GetUserInfo(const httplib::Request &req, httplib::Response &resp) {}

具体实现类似之前服务器对视频请求的处理,这里仅以Login进行举例:

static bool Login(const httplib::Request &req, httplib::Response &resp) {
    DLOG("recive login request");

    if(!req.has_file("identity") || !req.has_file("name") || !req.has_file("email") || !req.has_file("password")) {
        DLOG("login failed, lack of necessary information");
        set_error_response(resp, 500, "登录失败,缺少必要信息");
        return false;
    }
    
    Json::Value user_json;
    user_json["name"] = req.get_param_value("name");
    user_json["email"] = req.get_param_value("email");
    user_json["password"] = req.get_param_value("password");
    user_json["identity"] = req.get_param_value("identity");
    if(_tb_user->UserInfoMatching(user_json) == false) {
        DLOG("login failed, user information does not match");
        set_error_response(resp, 500, "登录失败,用户信息不匹配");
        return false;
    }

    std::string session_id = SessionUtil::generate_session_id();
    if(_tb_user->InsertSessionID(user_json["email"].asString(), session_id) == false) {
        DLOG("login failed, insert session id failed");
        set_error_response(resp, 500, "登录失败,插入session id失败");
        return false;
    }

    resp.status = 200;
    resp.body = R"({"result": true, "reason": "登录成功", "status": "success"})";
    resp.set_header("Content-Type", "application/json;");

    return true;
} // 登录用户

前端部分(待补充…)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值