C++学生信息和考试成绩管理系统(二)

C++学生信息和考试成绩管理系统

第一章 工程介绍

第二章 用户登陆准备和登陆

第三章 用户的增删查改

第四章 考试创建&成绩的增改

第五章 成绩查询



一、登陆准备

当前位置:main.cpp

	init_log();
	init_properties();

这两个函数是在process_file.cpp里面进行处理的,init_log()函数是用于初始化日志操作,日志的用途懂的都懂,本质上就是个文本,其实也不难。我写的日志格式也就是发布时间、发布等级、发布者、发布内容四要素。
日志
后一个init_properties()函数是用于初始化配置文件,在本工程中,配置文件的作用极为重要。如下图是配置文件init.ini的内容,可以看到主要有三项,账户信息文件位置、班级信息文件位置、考试记录文件位置,每一条都代表着对应内容的存储位置,当登陆时,会优先遍历前两条文件,检测是否有用户存在,用户信息等。当检查考试记录时,会优先遍历[record]下的文件,其中./是指相对本工程的目录下,文件存放位置,如有需要你也可以修改为其他地址,但不推荐。
目录树

可以看到,存放的位置是位于工程目录下/data文件夹下,也就是说,工程目录下有一个文件夹data,这个文件夹下有三个文件夹分别是configinforecord,其中,config下存放的是配置文件init.ini和日志文件log.log,在info下存放有两个文件夹账户文件夹account和班级文件夹class,分别存放了账户信息和班级信息,虽然后者并没有怎么用到。而在record下存放的都是考试文件,每一场考试为一个文件。上述除了config下的文件,都有一个共同点,那就是存储的后缀都是csv,存储格式都是ANSIGB2312
配置文件
下面具体说说这两个函数。
当前位置: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;
}

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值