C++学生信息和考试成绩管理系统
第三章 用户的增删查改
一、创建用户
继续,回到刚才管理员菜单界面,假设这时候我要创建一个用户,那就进入第1个菜单选项(第0个是结束程序,别以为我不知道)。这个是有点长,不过还能接受。
当前位置:manage_admin.cpp
void print_menu_admin_account_create(menu& menus)
{
while (true)
{
CLEAN;
// ---------- Start ----------
// 自定义函数:选择判断,输入非Y即N,否则返回-1并提示错误信息
switch (input_judgment(true, true, "Do you want to create an account? (Y/N): "))
{
case -1:
continue;
case 0:
return;
case 1:
default:
break;
}
// ---------- Print Menu & Select Privilege ----------
// 前面介绍了这个函数,打印菜单并提示内容
print_menu(true, 1, menus.vector_menu_create_account, "Please set a privilege or back: ");
// 前面介绍了这个函数,输入选项,不在范围内就要求重新输入
const int option = input_option(static_cast<int>(menus.vector_menu_create_account.size()));
switch (option)
{
case 0:
break;
case 1:
case 2:
{
auto* student = new student_info();// 新建并初始化一个学生类对象
// ---------- Select ----------
// 输入要注册的用户名
student->user_id = input(MAXSIZE_INPUT_USER_ID, true, "Please set account(user) name: ");
// 自定义函数:查找用户名是否存在,返回的是一个template_operation类对象,有成员info_flag为bool型
if (user_select(student->user_id)->info_flag)
{
print_wait("Cancelled because the user already exists!");
break;
}
// ---------- Input Information ----------
// 输入要注册的用户密码
student->user_password = input(MAXSIZE_INPUT_USER_PASSWORD, true, "Please set account(user) password: ");
// 输入权限
student->user_privilege = option;
// 输入学生姓名
student->name = input(MAXSIZE_STUDENT_NAME, true, "Please set administrator/student true name: ");
while (true)
{
// 输入学号
student->id = input(MAXSIZE_STUDENT_ID, true, "Please set it administrator/student id: ");
// 自定义函数:看看输入的是不是纯数字,我推荐用正则表达式,后面我大都是用正则,前面因为懒就随便写了下这个
if (is_positive_integer(student->id))
{
if (!user_select(student->id, 2)->info_flag)
{
break;
}
// 自定义函数:停止等待并提示
print_wait("Cancelled because the user already exists!");
free_ptr(student->id, true);// 自定义函数:释放学生类对象
continue;
}
free_ptr(student->id, true);
print_wait("Please input integer.");
}
while (true)
{
// 输入班级号
student->class_id = input(MAXSIZE_CLASS_ID, true, "Please set it administrator/student class id: ");
if (is_positive_integer(student->class_id)) // 看看输入的是不是纯数字,这个可以自己完成
{
break;
}
free_ptr(student->class_id, true);
print_wait("Please input integer.");
}
while (true)
{
// 输入性别
std::cout << "Please set administrator/student gender (0:Female, 1:Male): ";
student->gender = input_option(2);// 仅限输入0或1,分别代表女或男
if (0 == student->gender || 1 == student->gender)
{
break;
}
print_wait("Please input integer.");
}
// ---------- Reconfirm ----------
// 再次确认是不是输入对了
switch (input_judgment(true, true, "Are you sure you've filled in the information? (Y/N): "))
{
case -1:
free_ptr(student);
continue;
case 0:
print_wait("Canceled.");
free_ptr(student);
return;
case 1:
default:
break;
}
// ---------- Store Information ----------
if (!user_store(student))// 存储信息
{
print_wait("Canceled because save errors!");
free_ptr(student);
return;
}
// ---------- Print Information ----------
student->print_information();// 调用类对象的输出打印成员函数
print_wait("Created Successfully! Press any key to back...\n", false);
// ---------- Finish ----------
free_ptr(student);// 释放类对象
return;
}
default:
print_wait("Please input a correct option! ");
break;
}
}
}
介绍下里面提到的这些自定义函数。首先是input_judgment()
,这个函数用于判断输入的是Y还是N或者回车,不分大小写,提示内容,并返回结果,代码如下。
当前位置:format_input.cpp
int input_judgment(const bool is_print_hint, const bool is_default_error, ...)
{
if (is_print_hint)
{
va_list ap; // 这个读取变长参数操作也就这样,不停指针指向下一个地址,具体网上查查,这里不详细讲了
va_start(ap, is_default_error);
std::cout << va_arg(ap, const char*);
va_end(ap);
}
const char choice = input_char(); // 输入一个字符,自定义函数,其实也就是只读一个字符,避免换行符干扰
if (choice == 'Y' || choice == 'y' || choice == '\n')
{
return 1;
}
if (choice == 'N' || choice == 'n')
{
return 0;
}
if (is_default_error == true)
{
print_wait("Please input a correct option! ");
}
return -1;
}
上面那个应该不难,下面这个是学生类对象,其实下面这些本来是很多注释的,考虑到实际,所以都删了,只留了主要的。
当前位置:student_info.h
class student_info
{
public:
// 打印学生信息
void print_information();
// 设置账户前三要素
void set_student_account(const int get_user_privilege, char* get_user_password, char* get_user_id);
// 设置账户后四要素
void set_student_info(int get_gender, char* get_student_class, char* get_student_id, char* get_student_name, char* get_user_id);
// 用户名
char* user_id = nullptr;
// 密码
char* user_password = nullptr;
// 权限
int user_privilege = -1;
// 姓名
char* name = nullptr;
// 学号
char* id = nullptr;
// 班级号
char* class_id = nullptr;
// 性别
int gender = -1;
};
下面敲黑板了,算是一个小重点吧,也就是user_select()
函数。是不是看着一脸懵,像刚刚说的,operation_info<T*>
只是一个模板,需要传递类对象进去才有用,这个模板的作用主要也就是为这个类对象服务的。所以下面这个查找函数的返回类型是一个operation_info<student_info>*
,也就是学生类对象的模板。我姑且称之为操作类,也就是你看到的operation
,这个操作类在这个查找函数中修改了几个地方。
这么说吧,这个操作类里面有个info_flag
,默认是false
,找到用户后就返回true
,这个不难理解,有一个是info_path
,默认是nullptr
,查找成功后,会保存最后一个查找到的用户的所在记录文件account_xx.csv。假设这个用户是在账户文件2被查到的,那就是info_path = "./data/info/account/account_2.csv"
。
另外,有个成员变量是T* info_class = new T()
,里面装的就是自由类型,也就是传入的student_info
类对象并完成了初始化,当然,完成用户查询操作后,如果成功找到,那么这里面装的就是找到的那个学生的用户信息,包括用户名、密码、权限、姓名、学号、班级号、性别。
必须要强调下,这里我认为的班级号跟我们平常讲的班级号不一样,比如我们平常是4班、5班、6班,那只是一个序号,真正的班级号应该是行政班级号,也就是800864这种,或者干脆用学号前缀起,如这个学生的学号是2018070230421,那么他所在班级号是20180702304,学号是21
另外一个地方就是成员变量容器std::vector<T>
,这里面装的,也是学生类对象student_info
,但不同的是,这里装的是最后一次查找到相同用户名/学号的用户所在的文件下全部的学生类对象信息。
简单地说,上面那个是你要找的用户详细信息,下面那个是那个用户被找到的文件里包括他在内的所有用户详细信息。
当前位置:process_file.cpp
operation_info<student_info>* user_select(const char* uuid, const int mode)
{
auto* operation = new operation_info<student_info>; // 新建一个学生类对象的操作类
for (const auto& tmp_line : g_vector_file_path_account) // 遍历每一个用户文件
{
bool flag_select = false; // 查找成功的标志
std::string tmp_file = trim(tmp_line); // 去尾
std::fstream f;
try
{
f.open(tmp_file, std::ios::in | std::ios::binary); // 逐个打开用户文件
}
catch (std::exception&)
{
print_log("Not found file of accounts", severity_code_error);
}
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)) // 逐行读取
{
auto* student = new student_info();
// 下面这些都是分割并装入容器
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);
std::vector<const char*> tmp_vector_field;
while (token != nullptr)
{
if ('\r' == token[strlen(token) - 1] || '\n' == token[strlen(token) - 1])
{
token[strlen(token) - 1] = '\0';
}
tmp_vector_field.emplace_back(token);
token = strtok_s(nullptr, ",", &next_context);
}
switch (mode)
{
case 1:
case 2: // 根据传递的参数,判断是要检测用户名还是学号(因为这两个都是唯一标识符,所以具有权威性、独一性)
if ((1 == mode && 0 == strcmp(uuid, tmp_vector_field[0])) || (2 == mode && 0 == strcmp(uuid, tmp_vector_field[4])))
{
try
{
flag_select = true; // 找到了对应的用户标志置true
operation->info_flag = true; // 同时操作类的flag也置true,上面那个是临时的,这个要传出去的
char* tmp_str_line = new char[tmp_file.length() + 1];
strcpy_s(tmp_str_line, tmp_file.length() + 1, tmp_file.c_str());
// 注意,info_class类型是student_info
// 开始装入被找用户详细信息
operation->info_class->user_id = const_cast<char*>(tmp_vector_field[0]);
operation->info_class->user_password = const_cast<char*>(tmp_vector_field[1]);
operation->info_class->user_privilege = strtol(const_cast<char*>(tmp_vector_field[2]), nullptr, 0L);
operation->info_class->name = const_cast<char*>(tmp_vector_field[3]);
operation->info_class->id = const_cast<char*>(tmp_vector_field[4]);
operation->info_class->class_id = const_cast<char*>(tmp_vector_field[5]);
operation->info_class->gender = strtol(const_cast<char*>(tmp_vector_field[6]), nullptr, 0L);
operation->info_path = tmp_str_line;
}
catch (...)
{
flag_select = false;
operation->info_flag = false;
}
}
break;
default:
print_log("Error select user param. ", severity_code_error); // 打印日志,级别为错误级
f.close();
return operation;
}
try
{
// 开始装入找到用户的这个页面其他用户的详细信息
student->user_id = const_cast<char*>(tmp_vector_field[0]);
student->user_password = const_cast<char*>(tmp_vector_field[1]);
student->user_privilege = strtol(const_cast<char*>(tmp_vector_field[2]), nullptr, 0L);
student->name = const_cast<char*>(tmp_vector_field[3]);
student->id = const_cast<char*>(tmp_vector_field[4]);
student->class_id = const_cast<char*>(tmp_vector_field[5]);
student->gender = strtol(const_cast<char*>(tmp_vector_field[6]), nullptr, 0L);
}
catch (std::exception&)
{
print_log("Load information file errors!", severity_code_error);
operation->info_flag = false;
}
operation->info_vector_class.emplace_back(*student);
}
f.close();
if (flag_select)
{
return operation;
}
operation->info_vector_class.clear();
}
return operation; // 返回一个学生类对象的操作类
}
上面那个长吗?来看看这个短一点的,用户信息的存储,是不是很简单。反正传入的是学生类对象的容器,每一条都是一个学生信息记录,那我把这些信息逐行写入用户文件不就完了。
当前位置:process_file.cpp
bool user_store(std::vector<student_info>& vector_account, const char* path)
{
std::fstream f;
try
{
f.open(path, std::ios::out | std::ios::trunc | std::ios::binary);
if (!f.is_open())
{
return false;
}
for (const auto& tmp : vector_account)
{
// 因为是csv文件,所以必须要加逗号分隔,当然也可以不用逗号,但我们用户名总不会出现逗号(好像会),用它没错
f << tmp.user_id << ",";
f << tmp.user_password << ",";
f << tmp.user_privilege << ",";
f << tmp.name << ",";
f << tmp.id << ",";
f << tmp.class_id << ",";
f << tmp.gender << "\r\n";
}
f.close();
}
catch (const std::exception& e)
{
print_log(e.what(), severity_code_error);
return false;
}
return true;
}
我们回顾下,最开始我们先初始化了配置文件和日志文件,接着,我们输入用户名和密码,调用函数进行匹配,匹配成功为root用户后,进入了管理员菜单,菜单打印出来了,其次,我们选了创建用户这一选项,然后输入用户名、密码、权限、姓名等信息,这时候调用用户查找函数,看看是不是存在相同的用户名或者学号,如果没有,则开始调用存储函数,将用户信息持久化,并打印结果。
我们看看结果:
可以看到,因为我设置的名字宽度比较小,超出的名字长度将不被接受。来看看生成情况,看到最底下两个用户,证明创建用户成功。因为我当时没有把这个放进去,所以它没打印日志出来,懒得重新再演示一遍了,自己试看看就懂了,当然这个还得不断优化调整。
二、删除用户
来看看另一个删除函数,相似的,先给结果(图中是删除1002行的abcdef):
下载工程时优先下载时间最近的,最新的,因为之前的可能有些问题,比如删除时会增加空行,所以,还是保持新的为主。
下面是删除用户操作:
当前位置:manage_admin.cpp
void print_menu_admin_account_delete()
{
while (true)
{
CLEAN;
// ---------- Start ----------
switch (input_judgment(true, true, "Do you want to delete user? (Y/N) "))
{
case -1:
continue;
case 0:
return;
case 1:
default:
break;
}
const char* user_id = input(MAXSIZE_INPUT_USER_ID, true, "Please input the User ID to be deleted: ");
// ---------- Reconfirm ----------
std::cout << "Are you sure you want to delete \"" << user_id << "\"? (Y/N)" << std::endl;
switch (input_judgment())
{
case 0:
print_wait("Cancel.");
case -1:
free_ptr(user_id, true);
continue;
case 1:
default:
break;
}
// ---------- Select ----------
auto* operation = user_select(user_id);
if (!operation->info_flag)
{
print_wait("Cancel, the user id not exists!");
free_ptr(operation);
break;
}
// ---------- Delete ----------
operation = user_delete(operation);
if (!operation->info_flag)
{
print_log("Failed to delete user.", severity_code_error);
print_wait("Failed to delete user.");
break;
}
// ---------- Finish ----------
std::cout << "Delete user id: " << user_id << std::endl;
print_wait("Delete user account Successfully!");
free_ptr(operation);
free_ptr(user_id, true);
}
}
其中,删除函数user_delete()
具体为下面这段代码,无非就是把读取到的内容放到容器中增删查改,然后写回文件,仅此而已:
当前位置:process_file.cpp
operation_info<student_info>* user_delete(operation_info<student_info>* operation)
{
operation->info_flag = false;
std::fstream f;
try
{
f.open(operation->info_path, std::ios::in | std::ios::binary);
if (!f.is_open())
{
return operation;
}
std::string tmp_string;
std::vector<std::string> tmp_vector_line;
while (std::getline(f, tmp_string))
{
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);
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(operation->info_class->user_id, vector_tmp[0]))
{
tmp_vector_line.emplace_back(tmp_string);
}
}
f.close();
f.open(operation->info_path, std::ios::out | std::ios::trunc | std::ios::binary);
for (auto tmp : tmp_vector_line)
{
int index = static_cast<int>(tmp.length()) - 1;
while ((tmp[index] == '\r' || tmp[index] == '\n') && 0 < index)
{
tmp[index] = '\0';
index--;
}
f << tmp << "\n";
}
f.close();
operation->info_flag = true;
}
catch (const std::exception& e)
{
print_log(e.what(), severity_code_error);
}
return operation;
}
至此,增删用户完成,其实这些操作里还包含着查改的操作,所以准确地说应该是增删查改。班级信息的增删查改也大同小异,这里不再复述。