C++学生信息和考试成绩管理系统
第二章 用户登陆准备和登陆
一、登陆准备
当前位置:main.cpp
init_log();
init_properties();
这两个函数是在process_file.cpp
里面进行处理的,init_log()函数
是用于初始化日志操作,日志的用途懂的都懂,本质上就是个文本,其实也不难。我写的日志格式也就是发布时间、发布等级、发布者、发布内容四要素。
后一个init_properties()函数
是用于初始化配置文件,在本工程中,配置文件的作用极为重要。如下图是配置文件init.ini
的内容,可以看到主要有三项,账户信息文件位置、班级信息文件位置、考试记录文件位置,每一条都代表着对应内容的存储位置,当登陆时,会优先遍历前两条文件,检测是否有用户存在,用户信息等。当检查考试记录时,会优先遍历[record]
下的文件,其中./
是指相对本工程的目录下,文件存放位置,如有需要你也可以修改为其他地址,但不推荐。
可以看到,存放的位置是位于工程目录下/data文件夹下
,也就是说,工程目录下有一个文件夹data
,这个文件夹下有三个文件夹分别是config
、info
、record
,其中,config
下存放的是配置文件init.ini
和日志文件log.log
,在info
下存放有两个文件夹账户文件夹account
和班级文件夹class
,分别存放了账户信息和班级信息,虽然后者并没有怎么用到。而在record
下存放的都是考试文件,每一场考试为一个文件。上述除了config
下的文件,都有一个共同点,那就是存储的后缀都是csv
,存储格式都是ANSI
的GB2312
。
下面具体说说这两个函数。
当前位置:process_file.cpp
初始化日志函数:
void init_log()
{
if (_access("./data/config", 0))
{
_mkdir("./data/config");
}
g_ptr_log.open(PATH_FILE_LOGS, std::ios::out | std::ios::app | std::ios::binary);
if (!g_ptr_log.is_open() || g_ptr_log.fail())
{
print_log("Can't open log file!", severity_code_error);
exit(-1);
}
}
函数内,第一个if的作用就是看看这个文件夹在不在,不在就创建。然后是打开日志文件,不存在就创建,如果还是打不开,那整个程序默认退出。不然后续会有很麻烦的事。
初始化配置文件函数:
因为代码有点长,所以我把要说的放在注释里,便于观看,工程里没有这些,有些不好表述的放在代码后面。
void init_properties()
{
std::fstream f;
while (true)
{
try
{ // PATH_FILE_PROPERTIES -- 见后图,以只读方式打开配置文件,打不开就重新覆写
f.open(PATH_FILE_PROPERTIES, std::ios::in | std::ios::binary);
if (!f.is_open() || f.fail())
{
std::cout << "Initialize fail!" << std::endl;
print_log("Can't open properties.", severity_code_error);
// 以覆盖方式打开,因为文件不存在么(或者是打不开),反正太重要了这东西必须得打开,哪怕重新写
f.open(PATH_FILE_PROPERTIES, std::ios::out | std::ios::trunc | std::ios::binary);
// 文件是空的,写入标志位[account][class][record]这些
f << SIGN_PROPERTIES_ACCOUNT << "\r\n";
f << SIGN_PROPERTIES_CLASS << "\r\n";
f << SIGN_PROPERTIES_RECORD << "\r\n";
f.close();
}
else
{
break;
}
}
catch (std::exception& e)
{
std::cout << "Can't open properties!" << "\n" << e.what() << std::endl;
print_log("Initialize fail, can't open properties.", severity_code_error);
}
}
std::string tmp_line; // 打开后遍历每行,临时存放的行内容
// 读取到的行内容模式,=0则如果将读取到的行数放入全局变量账户容器,=1则放入班级容器,=2放入记录容器,都是全局变量
int line_mode = 0;
int count_db_account = 0; // 账户文件条数
int count_db_class = 0; // 班级文件条数
int count_db_record = 0; // 考试记录条数
while (std::getline(f, tmp_line))
{
// 这个trim()是自己写的函数,就是去掉尾部的\r和\n符号
std::string tmp = trim(tmp_line);
// 如果读取到的行内容为这个[account]则更改模式=0
if (0 == strcmp(tmp.c_str(), SIGN_PROPERTIES_ACCOUNT))
{
line_mode = 0;
continue;
}
// 如果读取到的行内容为这个[class]则更改模式=1
if (0 == strcmp(tmp.c_str(), SIGN_PROPERTIES_CLASS))
{
line_mode = 1;
continue;
}
// 如果读取到的行内容为这个[record]则更改模式=2
if (0 == strcmp(tmp.c_str(), SIGN_PROPERTIES_RECORD))
{
line_mode = 2;
continue;
}
if (!is_valid_file_path(tmp.c_str()))
{
continue;
}
if (is_valid_file_path(tmp.c_str()))
{
switch (line_mode)// 根据模式存放读取到的行内容到相应容器vector中,容器在头文件里声明了
{
case 0:
count_db_account++;
g_vector_file_path_account.emplace_back(tmp);
break;
case 1:
count_db_class++;
g_vector_file_path_class.emplace_back(tmp);
break;
case 2:
count_db_record++;
g_vector_file_path_record.emplace_back(tmp);
break;
default:
;
}
}
}
if (0 == count_db_account)// 如果为空,则打印到日志中提醒
{
print_log("Path list of account is empty.", severity_code_info);
}
if (0 == count_db_class)
{
print_log("Path list of class is empty.", severity_code_info);
}
if (0 == count_db_record)
{
print_log("Path list of record is empty.", severity_code_info);
}
f.close();
}
下面这个是去掉尾巴的函数trim()
(也在同一文件下定义和实现):
std::string trim(const std::string& str)
{
std::string tmp;
if ('\r' == str.c_str()[str.length() - 1] || '\n' == str.c_str()[str.length() - 1] || '\t' == str.c_str()[str.length() - 1])
{
tmp = str.substr(0, str.length() - 1);
}
else
{
tmp.assign(str);
}
return tmp;
}
里面看到的这些常量、宏定义,其实是在头文件定义好了的。
当前位置:process_file.h
二、登陆
这个是主函数内容:
当前位置:main.cpp
int main(int __argc, char* __argv[])
{
init_log(); // 初始化日志
init_properties(); // 配置读取初始化
try
{
menu menus = menu(); // 初始化了一个菜单类对象,里面基本满足了菜单内容
switch (login_verify()) // 这是个登陆验证函数,返回的是登陆用户的权限,在同一文件下
{
case privilege_admin:
print_menu_admin(menus);// 加载管理员界面
break;
case privilege_standard:
print_menu_standard_main(menus);// 加载标准用户界面
break;
case privilege_read:
print_menu_read_main(menus);// 加载只读/学生用户界面
break;
default:
return -1;
}
}
catch (std::exception& e)
{
print_log(e.what());
return -1;
}
再来具体看看这个login_verify()函数
:
中间的root可以忽略,那是专门测试用的超级管理员,也就是写死的。
int login_verify()
{
int attempts = 3;
while (attempts--)
{
CLEAN;// 宏定义 == system("cls")清除控制台内容
std::cout << "Welcome to Student Manage System\n" << std::endl;
try
{
// input()函数为自定义,需要输入指定长度,超过长度的输入将不会被读取
char* input_name = input(MAXSIZE_INPUT_USER_ID, true, "Please input UserName/ID: ");
char* input_password = input(MAXSIZE_INPUT_USER_PASSWORD, true, "Please input Password: ");
// Administrator ROOT PRIVILEGE
if (0 == strcmp("root", input_name) && 0 == strcmp("root", input_password))
{
// g_vector_login_info是存放已经登陆成功的当前用户信息,格式为用户存储7要素
g_vector_login_info.emplace_back("root"); // 用户名
g_vector_login_info.emplace_back("root"); // 密码
g_vector_login_info.emplace_back("0"); // 权限
g_vector_login_info.emplace_back("root"); // 姓名
g_vector_login_info.emplace_back("0000000000000"); // 学号
g_vector_login_info.emplace_back("000000"); // 班级号
g_vector_login_info.emplace_back("1"); // 性别
return privilege_admin;
}
// 这才是关注点
if (user_verify(input_name, input_password))// 这是另一个函数,专门用来验证用户的
{
std::cout << "Verify Successful!" << std::endl;
print_log("Login Successful.", severity_code_info, g_vector_login_info[0]);
Sleep(1000);
return strtol(g_vector_login_info[2], nullptr, 0L);/
}
print_sleep("Verify Failed! Please Try again!", 1500);// 自定义函数,打印提示内容并休眠若干时间
print_log("Failed to try login.", severity_code_info);// 自定义函数,日志打印
free_ptr(input_name, true);// 自定义模板函数,释放字符串
free_ptr(input_password, true);
}
catch (char)
{
print_wait("Error!");
exit(-1);
}
}
print_sleep("Sorry, you don't have chance! ", 3500, false, true);
exit(0);
}
顺带提一下里面涉及到的自定义函数,封装好的,并不复杂。
这个是输入函数,想仿照Python的input()
当前位置:format_input.cpp
char* input(const int max_str_length, const bool is_hint, ...)
{
// 从变长参数里面读取东西,其实这个可以不用变长参数,毕竟内容也就一个,但习惯性写了这个va_list
// 可以修改为一个默认参数,如果不为nullptr就输出,也不用后面的is_hint参数了(建议而已)
// 如果你知道Python的input(),那大概率你会知道这东西是哪来做什么的,区别就是传入了一个长度参数
va_list ap;
va_start(ap, is_hint);
if (is_hint)
{
std::cout << va_arg(ap, const char*);// 输出需要提示的内容
}
va_end(ap);
rewind(stdin);// 清空标准输入缓存区内容
const int count = max_str_length + 1;
char* str = new char[count];
fgets(str, count, stdin);// 读取缓存区内容
char* find_lf = strchr(str, '\n');
if (find_lf)
{
*find_lf = '\0';// 如果最后一个是换行符那就更改为\0
}
rewind(stdin);// 再次清空
return str;
}
这个是休眠函数,用于休眠一段时间并每隔一秒输出打印一个点,其实这个应该配合多线程来用的,在本工程用得并不多。
当前位置:format_print.cpp
void print_sleep(const char* tip, const int sleep_time, const bool have_br, const bool is_print_point)
{
// 其实就是问需不需要换行
have_br ? std::cout << tip << std::endl : std::cout << tip;
// 需不需要打点(打点计时器)
if (is_print_point)
{
const clock_t start = clock();
const int k_rate = 1000;
int base_time = 1001;
int base_response = 1000;
while (true)
{
if ((clock() - start) % base_time == base_response)
{
base_time += k_rate;
base_response += k_rate;
printf_s(".");
}
if (sleep_time < clock() - start)
{
printf_s("\n");
return;
}
}
}
Sleep(sleep_time);
}
下面这个是释放空间的模板函数:
当前位置:template_free_pointer.h
template <typename T>
void free_ptr(T* p, const bool is_array = false)
{
if (nullptr != p)
{
if (is_array)
{
delete[] p;
p = nullptr;
return;
}
delete p;
p = nullptr;
}
}
最后是登陆的关键:
当前位置:process_file.cpp
bool user_verify(const char* user_name, const char* user_password)
{
// 因为之前初始化已经把用户信息文件路径读取到了容器里,所以这里只需要遍历用户信息容器里所有路径即可
for (const auto& tmp_line : g_vector_file_path_account)
{
std::string tmp_file = trim(tmp_line);// 去尾
try
{
std::fstream f;
f.open(tmp_file, std::ios::in | std::ios::binary);// 打开容器里的第i个文件
if (!f.is_open())
{
print_log("Can't open an account file when load accounts to verify", severity_code_warning);
continue;
}
std::string tmp_string;// 临时变量,存放每一行内容
while (std::getline(f, tmp_string))// 读取每一行内容
{
// 下面是分割操作,因为是csv文件,便以英文逗号来分隔
char* context = new char[tmp_string.length() + 1];
char* next_context;
strcpy_s(context, tmp_string.length() + 1, tmp_string.c_str());
char* token = strtok_s(context, ",", &next_context);
// 分割完成后的内容装到一个临时的容器里,也就是这个vector_tmp
std::vector<const char*> vector_tmp;
while (token != nullptr)
{
if ('\r' == token[strlen(token) - 1] || '\n' == token[strlen(token) - 1])
{
token[strlen(token) - 1] = '\0';
}
vector_tmp.emplace_back(token);
token = strtok_s(nullptr, ",", &next_context);
}
// 验证输入的用户名和密码是否匹配,因为用户文件的格式是第一个用户名,第二个是密码,所以进入容器的前两个为所求
if (0 == strcmp(user_name, vector_tmp[0]) && 0 == strcmp(user_password, vector_tmp[1]))
{
// 如果找到了就把这容器里装的东西给全局变量容器,存放已经登陆的用户信息
g_vector_login_info.swap(vector_tmp);
f.close();
// 找到就返回成功
return true;
}
}
f.close();
}
catch (std::exception&)
{
print_log("File of accounts not found.", severity_code_error);
}
}
// 全部找完也没有就返回失败
return false;
}
完成验证后,就是跳转到相应的用户界面,这里因为是root是超管,所以跳到管理员界面。
print_menu_admin(menus);
这个函数便是打印菜单里的管理员界面到控制台,下面我们来看看这个函数实现过程,至于menus
装的是什么菜单,你就当作是一大堆字符串数组在里面就行。
当前位置:manage_admin.cpp
void print_menu_admin(menu& menus)
{
while (true)
{
CLEAN;
std::cout << "Welcome, Administrator\n" << std::endl;
print_menu(false, 1, menus.vector_admin_main, "Please select an option: ");// 打印菜单自定义函数
switch (input_option(static_cast<int>(menus.vector_admin_main.size()))) // 选项输入自定义函数
{
case 0:
std::cout << "Bye" << std::endl;
Sleep(800);
return;
case 1:
print_menu_admin_account_create(menus); // 创建用户
break;
case 2:
print_menu_admin_account_delete(); // 删除用户
break;
case 3:
print_menu_admin_create_class(); // 创建班级
break;
case 4:
print_menu_admin_delete_class(); // 删除班级
break;
case 5:
print_menu_admin_exam_create(menus); // 创建考试
break;
case 6:
print_menu_admin_exam_update(menus); // 更改成绩
break;
default:
print_wait("Please select an option!"); // 停止等待输出自定义函数
break;
}
}
}
上面的看注释应该能看出个大概吧,先介绍下这个“停止等待输出函数print_wait()
”,用途就是打印输出并等待,直到按下任意键则继续。
当前位置:format_print.cpp
void print_wait(const char* tip, const bool is_default_print)
{
if (is_default_print) // 这个是要不要打印提示内容,默认是true
{
std::cout << tip << " Press any key to continue..." << std::endl;
}
else
{
std::cout << tip;
}
_getch();
}
下面这个是打印菜单的函数,传入参数是个容器,函数作用就是把容器里面的东西打印出来并输出提示内容:
当前位置:format_print.cpp
void print_menu(const bool is_clean, const int tip_count, std::vector<const char*> vectors_str, ...)
{
if (is_clean)
{
CLEAN;
}
for (const char* str : vectors_str)
{
std::cout << str << std::endl;
}
printf_s("\n");
va_list ap;
va_start(ap, vectors_str);
for (int i = 0; i < tip_count; i++)
{
std::cout << va_arg(ap, const char*);
if (i < tip_count - 1)
{
printf_s("\n");
}
}
va_end(ap);
}
下面这个是输入选项函数,必须给个范围,我通常设置为容器(也就是菜单)的长度,如果输入非数字或超出范围则提示输出错误。
当前位置:format_input.cpp
int input_option(const int max_option_length)
{
const char* tmp = input(32);
if (!is_positive_integer(tmp))
{
return -1;
}
const int choice = strtol(tmp, nullptr, 0L);
if (choice < 0 || max_option_length - 1 < choice)
{
return -2;
}
return choice;
}