什么年代还在写ATM系统🧐🧐🧐

112 阅读38分钟

项目背景

随着银行业务的发展和客户需求的多样化,传统的银行管理方式已经不能满足现代化的要求,需要使用计算机技术来提高银行的效率和服务质量。因此,本项目旨在开发一个基于C语言的银行管理系统,实现对银行用户和管理员的信息管理和业务处理。

项目目标

本项目的主要目标是:

  • 为银行用户提供一个方便、安全、高效的平台,实现注册、登录、存款、取款、转账、查询余额、修改密码、修改个人信息、余额变动记录、用户日志记录等功能。
  • 为银行管理员提供一个简洁、易用、智能的工具,实现管理员登录、查看所有用户、查看单个用户信息、查看银行总体金额变化、帮助用户修改密码、管理员日志记录等功能。
  • 为银行提供一个稳定、可靠、可扩展的系统,实现对用户和管理员的数据存储、检索、更新、备份等功能。

项目特色

本项目的主要的特色是:

融会贯通地使用了C语言初级课程中各种复杂结构,结构体,指针,哈希表,链表,文件,函数模块化。

1. 基础知识准备

建议初学者还是先学习C语言基础知识,这里只做简单的回顾。

1.1. 指针

指针是C语言的一种变量类型,它存储了一个内存地址。指针变量的值是一个地址,该地址指向内存中的另一个位置,而这个位置可能存储一个值或者另一个指针。

指针放在开头就证明了它在C语言中的重要性,同时也是劝退百分之五十的初学者学习C语言的拦路虎,大家要认真学呀,指针在C语言用的还是很频繁的,学透了指针也会给我们开发带来大大的方便。

指针的主要作用是允许程序间接访问内存,从而实现高效的数据操作。

指针变量的定义形式为:

数据类型 *指针变量名;

其中,数据类型表示指针所指向的数据类型,*表示指针类型,指针变量名是指针变量的名称。例如,int *p;表示定义了一个指向整型变量的指针p。

指针是初学者学习C语言的一个槛,所以在这里强调一下指针变量的使用需要注意的一些点:

  1. 指针变量必须先定义再使用;
  2. 指针变量必须初始化后才能使用,否则就是野指针;
  3. 指针变量可以通过*运算符来访问其指向的内存地址上的值,通过&运算符来获取某个变量的地址,*&是一对相反的操作;
  4. 指针变量可以进行算术运算,表示地址的变化;
  5. 指针变量可以作为函数参数传递,从而实现函数间的数据共享;

1.2. 结构体

结构体是C语言的一种自定义的数据类型,它允许用户将不同类型的数据组合成一个整体。

结构体的定义形式为:

image.png

其中,struct是关键字,是结构体类型的标志。结构体内部的元素,也是组成成分,一般称为成员。

结构体的使用频率不亚于指针,因此需要重视,结构体的定义和使用非常简单,以下是结构体的定义和使用代码:

image.png

在上面的示例中,定义了一个名为Student的结构体,它三个成员组成:nameageheight。可以通过.运算符来访问结构体的成员。

1.3. 哈希表

哈希表是一种数据结构,它可以用于快速查找和插入数据。哈希表是由若干个键值对组成,每个键值对包含了一个键和一个值。

哈希表的主要特点是可以通过键快速查找对应的值,而不需要遍历整个数据集。哈希表的实现原理是将键通过哈希函数转化为一个索引,然后将值存储在该索引对应的位置上。哈希表的主要优点是查找和插入的时间复杂度都是常数级别,因此可以实现高效的数据操作。

哈希表的实现需要考虑以下几个方面:

  1. 哈希函数的设计:哈希函数是将键映射到哈希表中的索引的函数。一个好的哈希函数应该尽可能的将键均匀的映射到哈希表中的索引,以避免哈希冲突。
  2. 哈希表的冲突处理:由于哈希函数的设计不可能完全避免哈希冲突,因此需要考虑如何处理哈希冲突。常见的哈希冲突处理方法包括链地址法,开放地址法等;
  3. 哈希表的扩容:当哈希表中的元素数量超过了哈希表大小的一定比例时,需要对哈希表进行扩容,以保证哈希表的性能;

下面是哈希表的定义和创建代码:

image.png

这个项目使用的哈希表结构,是用数组+链表实现的哈希表拉链法,便于处理哈希冲突,在网上扒拉下一张图片,如下(侵权删)。

1.4. 链表

链表是一种重要的数据结构,它可以动态的分配内存地址,实现高效的数据操作。链表由一系列的节点构成,每个节点包含了数据和指向下一个节点的指针。

链表的特点:

  1. 解决数组无法存储多种数据类型的问题;
  2. 解决数组中,元素个数无法改变的限制;
  3. 数组移动元素的过程中,要对元素进行大范围的移动,很耗费时间,效率也不高;

链表的基本构成是头指针(head),若干个节点,且最后一个节点要指向空。

链表的实现原理是头指针指向链表的第一个节点,然后第一个节点中的指针指向下一个节点,然后一次指到最后一个节点,这样就构成了一条链表。

链表的数据结构定义如下:

image.png

1.5. 文件

C语言的文件是一种用于存储数据的抽象概念。文件可以是文本文件或二进制文件。

在C语言中,我们可以通过I/O函数来对文件进行读写操作。相关的操作包括打开、读写、关闭和设置缓冲区。

下面是一个简单的文件读写操作的例子:

image.png

fprintf() fputs() 函数向文件中写入数据。然后,使用fscanf() fgets() 函数从文件中读取数据,并使用printf() 函数将其输出到控制台上。

需要注意的是,在进行文件读写操作之前,需要先打开文件,使用完毕后再关闭文件。

1.6. 函数模块化

模块化编程是一种将程序分割成模块并进行编程的技术。在C语言中,模块是由函数和数据组成的逻辑单元,模块化编程的好处包括代码可读性和可维护性的提高,程序的复杂性降低,便于代码重用等。

在C语言中,可以通过函数来实行模块化编程。一个C语言程序可以由一个主函数和若干其他函数构成。函数的定义和调用可以使程序更加清晰和易于维护。

2. 用户板块设计

2.1. 结构体等一些准备

下面是这个项目关于用户信息的结构体定义。

image.png 账户卡号唯一id,是以哈希表存储的键,其与身份证密切相关的。

2.2. 注册

用户注册:用户可以输入自己的姓名、手机号码、身份证号码、密码等信息,创建一个银行账户,并获得一个唯一的账户卡号。

  1. 首先,注册函数定义了一个字符数组 pw 用于存储用户输入的确认密码,以及一个字符串 oper 用于记录操作类型(这里是 “注册”)。
  2. 然后,函数动态分配了两个结构体变量的内存空间:data 用于存储 DataType 类型的数据(包括键和值,其中值是 Customer 类型),user 用于存储注册用户的所有信息。
  3. 函数提示用户输入姓名、手机号码和身份证号码,并将这些信息存储在 user 结构体中。然后,函数将字符串类型的身份证号码转换为数字类型的银行账号,并存储在 user 结构体中。
  4. 如果哈希映射中已经存在该账号,函数会提示用户该用户已存在并结束注册过程。
  5. 如果哈希映射中不存在该账号,函数会提示用户注册成功,并显示用户的银行卡号。
  6. 接下来,函数会提示用户输入银行卡密码(6位数),并要求用户再次输入以确认。如果两次输入的密码不一致,函数会提示用户重新输入。
  7. 当用户成功输入一致的密码后,函数会将用户的余额设置为0,并获取当前时间作为用户创建时间。
  8. 然后,函数将用户的银行账号和用户信息存储在 data 结构体中,并将 data 存储在哈希映射和链表中。
  9. 最后,函数会将用户信息写入文件,并记录用户操作日志。如果两次输入的密码一致,函数会提示用户银行卡注册成功,并释放 user 结构体的内存空间。
void sign(){
	char pw[7];  // 用于确认密码
	char *oper = "注册";
	DataType* data = (DataType*)malloc(sizeof(DataType));
	Customer* user = (Customer*)malloc(sizeof(Customer)); // 存储注册用户的所有信息

	printf("欢迎来到注册界面\n");	
	printf("请依次输入您的名字  手机号码  身份证:\n");
	scanf("%s %s %s",&user->accountName,&user->mobile,&user->sfz);
	user->accountCard = atoi(user->sfz); // 将字符串类型的身份证转化为数字类型的银行账号

	if(constainsKey(hashmap,user->accountCard)){
		printf("用户已存在,请去登录\n");
		return;
	}

	printf("注册成功,您的银行卡号为:%d\n",user->accountCard);
	while(1){
		printf("请输入您的银行卡密码(6位数):\n");
		scanf("%s",&user->password);
		printf("请再次输入你的银行卡密码:\n");
		scanf("%s",&pw);
		if(strcmp(user->password,pw) != 0){
			printf("两次输入密码不一致,请重新输入\n");
			continue;
		}else{
			break;
		}
	}
	user->money = 0.0;  // 注册的时候用户的余额为0
	
	currentTime(user->Time);  // 获取当前时间存入user中,表示创建用户的时间
	data->key = user->accountCard;
	data->value = (Customer *) malloc(sizeof(Customer));
    memcpy(data->value, user, sizeof(Customer));  // 将注册用户信息内容复制到data->value中

	putData(hashmap,data); // 将刚刚注册的用户信息存储到hashmap中
	addNode(head,data); // 将刚刚注册的用户信息存储到head中
	user_filePut(user);
	userLogsRecords(user,oper);
	printf("两次输入密码正确,银行卡注册成功!\n");
	free(user);  // 为什么可以free掉,也忘记了
	return;
}

2.3. 用户登录

用户登录:用户可以输入自己的账户卡号和密码,登录到自己的账户,进行后续的操作。

  1. 用户登录函数首先定义了一个整数 accountCard 用于存储用户输入的账号,以及一个字符数组 password 用于存储用户输入的密码。此外,函数还定义了一个字符串 oper 用于记录操作类型(这里是 “登录”)。
  2. 函数输出欢迎信息,并进入一个无限循环,提示用户输入银行卡号和密码,知道登录正确的账号就跳出循环。
  3. 函数使用 getData 函数检查哈希映射中是否存在用户输入的账号。如果存在,函数将获取到的用户信息赋值给 custCurrent
  4. 接下来,函数使用 strcmp 函数比较用户输入的密码和 custCurrent 中存储的密码。如果两者相同,函数将用户信息记录到日志文件中,并输出登录成功的信息,然后结束函数。如果两者不同,函数将输出密码输入错误的信息,并重新开始循环。
  5. 如果哈希映射中不存在用户输入的账号,函数将输出账号和密码输入错误的信息,并重新开始循环。
// 登录
void login(){
	int accountCard;  // 存储登录用户输入的账号,用于后续做账号比较
	char password[7];  // 存储登录用户输入的密码,用于后续做密码比较
	char *oper = "登录";  // 登录操作,写入日志文件
	printf("欢迎来到登录界面\n");
	while(1){
		printf("请输入您的银行卡号和密码:\n");
		scanf("%d %s",&accountCard,&password);
		// 去记录中查找,检查1.是否存在此卡号;2.密码是否正确
		if((Customer*)getData(hashmap,accountCard)!=NULL){
			// 接下来检查密码是否输入正确
			custCurrent = (Customer*)getData(hashmap,accountCard); // 查找成功,就将查找到的信息赋值一份给custCurrent,便于后面一直拿到用户的信息
			if(strcmp(password,custCurrent->password)==0){
				// 将用户信息记录到日志logs记录文件中
				userLogsRecords(custCurrent,oper);
				printf("登录成功,请继续接下来的操作\n");
				return;	
			}else{
				printf("密码输入错误!请重新操作\n");
			}
		}else{
			printf("银行卡号和密码输入错误,请重新输入:\n");
			continue;
		}
	}
}

2.4. 存款

存款:用户可以输入存款金额,将现金存入自己的账户,账户余额会相应增加。

  1. saveMoney 函数:
  • 这个函数的主要功能是存款。首先,函数定义了一个字符串 oper 用于记录操作类型(这里是 “存款”),以及一个浮点数 money 用于存储用户要存款的金额。
  • 然后,函数从哈希映射中获取当前用户的信息,并将其存储在 user 结构体中。
  • 函数提示用户输入要存储的金额(只能存储整百元),并将输入的金额存储在 money 中。
  • 如果用户输入的金额是整百数,函数会将这个金额添加到用户的余额中,并更新链表和文件中的用户信息。然后,函数会将操作记录到日志文件中,并更新银行的总金额。最后,函数会输出存款成功的信息,并显示用户的银行卡号和当前余额。
  • 如果用户输入的金额不是整百数,函数会提示用户重新输入整百的金额。
  1. saveMoneyLogical 函数:
  • 这个函数的主要功能是实现存款的逻辑业务。首先,函数定义了两个整数 iflag,并输出欢迎信息。
  • 然后,函数进入一个循环,提示用户选择存款或退出,并将用户的选择存储在 i 中。
  • 如果用户选择存款,函数会调用 saveMoney 函数。如果用户选择退出,函数会结束循环。如果用户的输入不是 1 或 2,函数会提示用户输入有误,并重新开始循环。
// 存款
void saveMoney(){
	char *oper = "存款";
	float money;
	Customer *user = (Customer*)getData(hashmap,custCurrent->accountCard);
	user = custCurrent;
	printf("欢迎来到存款界面\n");
	printf("请输入您要存储的钱(只存储整百元)\n");
	scanf("%f",&money);

	if(((int)money)/100 != 0 && ((int)money)%100 == 0){
		custCurrent->money += money;
		updateNode(head,custCurrent,user,sizeof(custCurrent));//将更新后的用户信息,也在链表中更新一份
		// 修改的信息存入源信息文件时,需要原来的时间,从链表中取出的时间有问题
		userUpgrade_filePut(head);
		userLogsRecords(custCurrent,oper);
		Bank_TotalAmountChange(oper,custCurrent,money);
		// 修改的信息应该存入日志文件中
		printf("存储成功!\n");
		printf("您的银行卡号为:%d,现在的余额为:%f\n",custCurrent->accountCard,custCurrent->money);
	}else{
		printf("您存储的钱不是整百数,请重新存储整百的钱\n");
	}
	return;
}

// 存款逻辑业务
void saveMoneyLogical(){
	int i=0,flag=1;
	printf("欢迎来到存款界面\n");
	while(flag){
		printf("1 存款\n");
		printf("2 退出\n");
		scanf("%d",&i);
		switch(i){
		case 1:saveMoney();break;
		case 2:flag=0;break;
		default:printf("输入有误,请重新输入!\n");break;
		}
	}
	return;
}

2.5. 取款

取款:用户可以输入取款金额,从自己的账户中取出现金,账户余额会相应减少。

  1. drawMoney 函数:
  • 这个函数的主要功能是取款。首先,函数定义了一个字符串 oper 用于记录操作类型(这里是 “取款”),以及一个整数 money 用于存储用户要取款的金额。
  • 然后,函数从哈希映射中获取当前用户的信息,并将其存储在 user 结构体中。
  • 函数提示用户输入要取款的金额(只能取款整百元),并将输入的金额存储在 money 中。
  • 如果用户输入的金额是整百数,函数会检查用户的余额是否足够。如果足够,函数会从用户的余额中扣除取款金额,并更新链表和文件中的用户信息。然后,函数会将操作记录到日志文件中,并更新银行的总金额。最后,函数会输出取款成功的信息,并显示用户的银行卡号和当前余额。如果余额不足,函数会提示用户余额不足,并要求用户重新操作。
  • 如果用户输入的金额不是整百数,函数会提示用户重新输入整百的金额。
  1. drawMoneyLogical 函数:
  • 这个函数的主要功能是实现取款的逻辑业务。首先,函数定义了两个整数 iflag,并输出欢迎信息。
  • 然后,函数进入一个循环,提示用户选择取款或退出,并将用户的选择存储在 i 中。
  • 如果用户选择取款,函数会调用 drawMoney 函数。如果用户选择退出,函数会结束循环。如果用户的输入不是 1 或 2,函数会提示用户输入有误,并重新开始循环。
// 取款
void drawMoney(){
	char *oper = "取款";
	int money;
	Customer *user = (Customer*)getData(hashmap,custCurrent->accountCard);
	user = custCurrent;
	printf("请输入您需要取款的金额(只能取款整百元)\n");

	scanf("%d",&money);
	if(((int)money)/100 != 0 && ((int)money)%100 == 0){
		if(money <= custCurrent->money){
			printf("您的余额足够,可以为您取出\n");
			custCurrent->money -= money;
			userUpgrade_filePut(head);
			userLogsRecords(custCurrent,oper);
			Bank_TotalAmountChange(oper,custCurrent,money);
			printf("取款成功!\n");
			printf("您的银行卡号为:%d,现在的余额为:%f\n",custCurrent->accountCard,custCurrent->money);
		}else{
			printf("您的余额不足,无法取出您需要的钱,请重新操作\n");
		}
	}else{
		printf("您取出的钱不是整百,请重新输入\n");
	}
}

// 取款逻辑业务
void drawMoneyLogical(){
	int i=0,flag=1;
	printf("欢迎来到取款界面\n");
	while(flag){
		printf("1 取款\n");
		printf("2 退出\n");
		scanf("%d",&i);
		switch(i){
		case 1:drawMoney();break;
		case 2:flag=0;break;
		default:printf("输入有误,请重新输入!\n");break;
		}
	}
	return;
}

2.6. 转账

转账:用户可以输入对方的账户卡号和转账金额,将自己的账户中的钱转给对方,自己的账户余额会相应减少,对方的账户余额会相应增加。

  1. 转账函数首先定义了两个字符串 operoper1 用于记录操作类型(这里是 “转账”),以及一个 Customer 类型的指针 custTMP_1 用于存储接收转账的用户信息。
  2. 然后,函数从哈希映射中获取当前用户的信息,并将其存储在 user 结构体中。
  3. 函数输出欢迎信息,并进入一个无限循环,提示用户输入接收转账的账号和姓名。
  4. 函数使用 getData 函数检查哈希映射中是否存在用户输入的账号。如果存在,函数将获取到的用户信息赋值给 custTMP_1
  5. 接下来,函数使用 strcmp 函数比较用户输入的姓名和 custTMP_1 中存储的姓名。如果两者相同,函数会输出接收转账的用户名,并进入另一个无限循环。
  6. 在这个循环中,函数提示用户输入转账金额,并要求用户确认转账。如果用户确认转账,函数会检查用户输入的金额是否是整百数,以及用户的余额是否足够。
  7. 如果用户输入的金额是整百数且余额足够,函数会从用户的余额中扣除转账金额,并将转账金额添加到 custTMP_1 的余额中。然后,函数会更新链表和文件中的用户信息,并将操作记录到日志文件中。最后,函数会输出转账成功的信息,并结束函数。
  8. 如果用户输入的金额不是整百数或者余额不足,函数会提示用户重新输入,并重新开始循环。
  9. 如果用户输入的姓名和 custTMP_1 中存储的姓名不一致,函数会提示用户姓名输入错误,并重新开始循环。
  10. 如果哈希映射中不存在用户输入的账号,函数会提示用户账号不存在,并结束函数。
// 转账--目前只能本行转账本行
void transferMoney(){
	char oper[20] = "转账";
	char oper1[20] = "转账";
	Customer* custTMP_1;
	int flag,accountCard;
	char accountName[10];
	float money=0;
	Customer *user = (Customer*)getData(hashmap,custCurrent->accountCard);
	user = custCurrent;

	printf("欢迎来到转账界面\n");
	while(1){
		printf("请输入您需要转账的账号 姓名\n");
		scanf("%d %s",&accountCard,&accountName);
		if((custTMP_1 = (Customer*)getData(hashmap,accountCard)) != NULL){
			// 账号存在,开始校验姓名,get函数返回对应卡号的用户信息
			if(strcmp(custTMP_1->accountName,accountName) == 0){
				printf("你要转账的用户名为:%s\n",custTMP_1->accountName);
				while(1){
					printf("请输入转账金额:\n");
					scanf("%f",&money);
					// 在这里确认转账其实也可以做一个界面(优化)
					printf("确认转账请输入1,输入其他取消转账\n");
					scanf("%d",&flag);
					if(flag==1){
						// 继续校验自己的账户是否有足够转账的金额
						if(((int)money)/100 != 0 && ((int)money)%100 == 0){
								if((custCurrent->money - money) >= 0){
									custCurrent->money -= money;
									custTMP_1->money += money;
									userUpgrade_filePut(head);
									userLogsRecords(custCurrent,oper);
									Bank_TotalAmountChange(strcat(oper,"发起方"),custCurrent,money);
									Bank_TotalAmountChange(strcat(oper1,"接收方"),custTMP_1,money);
									printf("转账成功!");
									return;
								}else{
									printf("您的账户上余额不足,请重新输入\n");
									continue;
								}
						}else{
							printf("请输入整百转账金额\n");
						}
					}else{
						printf("取消转账!\n");
						return;
					}
				}
			}else{
				printf("您输入银行账号对应的账号姓名不对,请重新输入\n");
				continue;
			}
		}else{
			printf("您输入的账号不存在!");
			return;
		}
	}
}

2.7. 查询余额

查询余额:用户可以查看自己的账户余额,以及最近的余额变动记录。

  1. 查询余额函数首先定义了一个字符串 oper 用于记录操作类型(这里是 “查询余额”)。
  2. 然后,函数输出欢迎信息,并显示用户的银行卡号和当前余额。
  3. 最后,函数将操作记录到日志文件中。
// 查询余额
void showMoney(){
	char *oper = "查询余额";
	printf("欢迎来到查询余额界面\n");

	printf("您的银行卡号为:%d\n",custCurrent->accountCard);
	printf("您的余额为:%lf\n",custCurrent->money);
	userLogsRecords(custCurrent,oper);
	return;
}

2.8. 修改密码

修改密码:用户可以输入旧密码和新密码,修改自己的账户密码。

  1. 函数首先定义了一个字符串 oper 用于记录操作类型(这里是 “修改密码”),以及两个字符数组 passWordpassWord_1 用于存储用户输入的原密码和新密码。
  2. 然后,函数从哈希映射中获取当前用户的信息,并将其存储在 user 结构体中。
  3. 函数输出欢迎信息,并进入一个无限循环,提示用户输入原密码。
  4. 函数使用 strcmp 函数比较用户输入的原密码和 user 结构体中存储的密码。如果两者相同,函数会提示用户输入新密码。
  5. 如果用户输入的新密码和原密码相同,函数会提示用户不能修改为原密码,并重新开始循环。如果用户输入的新密码和原密码不同,函数会结束循环。
  6. 如果用户输入的原密码和 user 结构体中存储的密码不一致,函数会提示用户原密码输入错误,并重新开始循环。
  7. 当用户成功输入一个与原密码不同的新密码后,函数会使用 strcpy 函数将新密码复制到 user 结构体中,并更新链表和文件中的用户信息。然后,函数会将操作记录到日志文件中。
  8. 最后,函数会输出密码修改成功的信息。
// 修改密码
void changePW(){
	char *oper = "修改密码";
	char passWord[7],passWord_1[7];
	Customer *user = (Customer*)getData(hashmap,custCurrent->accountCard);

	printf("欢迎来到修改密码界面\n");
	while(1){
		printf("请输入原密码:\n");
		scanf("%s",&passWord);

		if(strcmp(passWord,user->password) == 0){
			printf("请输入你想要修改的密码:\n");
			scanf("%s",&passWord_1);
			if(strcmp(passWord,passWord_1)==0){
				printf("不能修改为原密码!请重新操作\n");
				continue;
			}else{
				break;
			}
		}else{
			printf("原密码输入错误!请重新操作\n");
			continue;
		}
	}
	strcpy(user->password,passWord_1);
	custCurrent = user;
	userUpgrade_filePut(head);
	userLogsRecords(user,oper);
	printf("密码修改成功!\n");
}

2.9. 修改个人信息

修改个人信息:用户可以修改自己的姓名、手机号码、身份证号码等信息,但不能修改账户卡号。

  1. 函数首先定义了一个字符串 oper 用于记录操作类型(这里是 “修改用户信息”),以及两个整数 iflag
  2. 然后,函数从哈希映射中获取当前用户的信息,并将其存储在 user 结构体中。
  3. 函数输出欢迎信息,并显示用户的银行卡号、账户名称、手机号码、身份证号码、密码(以星号表示)和余额。
  4. 函数进入一个循环,提示用户选择要修改的信息(账户名称、电话号码或密码),并将用户的选择存储在 i 中。
  5. 如果用户选择修改账户名称,函数会提示用户输入新的账户名称,并将其存储在 user 结构体中。如果用户选择修改电话号码,函数会提示用户输入新的电话号码,并将其存储在 user 结构体中。如果用户选择修改密码,函数会提示用户输入新的密码,并将其存储在 user 结构体中。
  6. 如果用户选择退出修改个人信息界面,函数会结束循环。如果用户的输入不是 1、2、3 或 0,函数会提示用户输入有误,并重新开始循环。
  7. 在每次循环结束时,函数都会显示用户的银行卡号、账户名称、手机号码、身份证号码、密码(以星号表示)和余额。
  8. 当用户完成修改后,函数会更新链表和文件中的用户信息,并将操作记录到日志文件中。
  9. 最后,函数会输出修改用户信息成功的信息,并暂停程序执行。
// 修改用户信息 
void updateInfo(){
	char *oper = "修改用户信息";
	int i=0;
	int flag = 1;
	Customer *user = (Customer*)getData(hashmap,custCurrent->accountCard);

	printf("欢迎来到修改个人信息界面:\n");
	printf("您的信息为:");
	printf("银行卡号:%d,账户名称:%s,手机号码:%s,身份证:%s,密码:******,余额:%lf\n\n",user->accountCard,user->accountName,user->mobile,user->sfz,user->money);
	
	while(flag){
		printf("请问你要修改什么信息?\n");
		printf("1 修改账户名称\n");  // 有些人改名了
		printf("2 修改电话号码\n");
		printf("3 修改密码\n");  // 认为在修改信息的时候,密码也属于个人信息,应该兼容
		printf("0 退出修改个人信息界面\n");

		scanf("%d",&i);
		// 修改的信息如果是一样的没有校验!
		switch(i){
		case 1:printf("请输入账户名称:\n");scanf("%s", &user->accountName);break;
		case 2:printf("请输入电话号码:\n");scanf("%s", &user->mobile);break;
		case 3:printf("请输入要修改的密码:\n");scanf("%s", &user->password);break;
		case 0:flag = 0;break;
		default:printf("输入有误,请输入对应的值");
		}
		printf("您的信息为:");
		printf("银行卡号:%d,账户名称:%s,手机号码:%s,身份证:%s,密码:******,余额:%lf\n\n",user->accountCard,user->accountName,user->mobile,user->sfz,user->money);
	}

	custCurrent = user;
	userUpgrade_filePut(head);
	userLogsRecords(user,oper);
	printf("修改用户信息成功! ");
	system("pause");
	return;
}

2.10. 余额变动记录

余额变动记录:当用户的操作会改变自己的余额时,就会将此次操作记录到日志文件中。

  1. 函数首先定义了一个 FILE 类型的指针 fp 用于操作文件,一个字符数组 time 用于存储时间,两个浮点数 moneyBankTotalMoney 用于存储操作的金额和银行总体金额,一个字符数组 oper 用于存储操作类型,以及一个整数 accountCard 用于存储账号。
  2. 然后,函数使用 fopen 函数以读取模式打开名为 “银行总金额变化文件.txt” 的文件,并将返回的文件指针赋值给 fp。如果文件打开失败,函数会输出错误信息,并暂停程序执行。
  3. 函数输出提示信息,并进入一个循环,使用 feof 函数检查是否已经到达文件末尾。
  4. 在循环中,函数使用 fscanf 函数从文件中读取时间、账号、操作类型、操作的金额和银行总体金额,并将这些信息存储在相应的变量中。
  5. 如果读取到的账号与当前用户的账号相同,函数会输出时间、账号、操作类型和操作的金额。
  6. 当到达文件末尾时,循环结束。
  7. 最后,函数使用 fclose 函数关闭文件。
// 从银行总金额变化文件中获取当前用户全部的信息
void getUserBalanceChangeFromFile() {
    FILE *fp;
    char time[30];
	double money = 0.0; // 操作的金额
	double BankTotalMoney = 0.0; // 银行总体金额
	char oper[20];
    int accountCard = 0;

    fp = fopen("银行总金额变化文件.txt", "r");
    if (fp == NULL) {
        perror("打开文件失败啦");
        system("pause");
        exit(1);
    }

	printf("您的余额变动记录为:\n");
    while (!feof(fp)) {
		fscanf(fp,"%s %d %s %lf %lf\n",&time,&accountCard,&oper,&money,&BankTotalMoney);
		if(custCurrent->accountCard == accountCard){
			printf("%s %d %s %lf\n",time,accountCard,oper,money);
		}
	}

    fclose(fp);
}

2.11. 用户日志记录

用户日志记录:用户的每一次操作都会被记录到一个文件中,包括操作时间、操作类型、操作金额等信息,方便用户查看自己的操作历史。

  1. 用户日志登录函数首先定义了一个 FILE 类型的指针 fp 用于操作文件,以及一个字符数组 time 用于存储当前时间。
  2. 然后,函数使用 fopen 函数以追加模式打开名为 “用户logs日志文件.txt” 的文件,并将返回的文件指针赋值给 fp
  3. 函数调用 currentTime 函数获取当前时间,并将返回的时间字符串存储在 time 中。
  4. 接下来,函数使用 fprintf 函数将当前时间、用户的银行卡号、用户的姓名和操作类型写入文件。
  5. 最后,函数使用 fclose 函数关闭文件。
// 用户的日志logs记录
void userLogsRecords(Customer *custTmp,char *oper){
	FILE *fp;
	char time[30];
	fp = fopen("用户logs日志文件.txt","a");
	currentTime(time);
	fprintf(fp,"%s %d %s %s\n",time,custTmp->accountCard,custTmp->accountName,oper);
	fclose(fp);
	return;
}

3. 管理员板块设计

3.1. 管理员登录

管理员登录:管理员暂时只能输入输入默认的账户卡号和密码,登录到管理员的账户,进行后续的操作。

  1. 管理员登录函数首先定义了两个字符数组 adminName_loginadminPW_login,用于存储管理员的账号和密码。
  2. 然后,函数进入一个无限循环,直到管理员成功登录后才会跳出循环。
  3. 在循环中,函数首先使用 printf 函数提示管理员输入账号,并使用 scanf 函数读取输入的账号,存储到 adminName_login 中。
  4. 接下来,函数同样使用 printfscanf 函数提示管理员输入密码,并将输入的密码存储到 adminPW_login 中。
  5. 然后,函数使用 strcmp 函数比较输入的账号和密码是否与预设的管理员账号和密码相同。
  6. 如果账号和密码都正确,那么函数使用 printf 函数打印出 "管理员登录成功" 的信息,并通过 break 语句跳出循环。
  7. 如果账号或密码有误,那么函数使用 printf 函数打印出 "账号或密码错误" 的信息,并继续下一轮的循环,重新输入账号和密码。
// 管理员的登录
void adminLogin(){
	char adminName_login[5];
	char adminPW_login[5];
	while(1){
		printf("请输入管理员账号:\n");
		scanf("%s",adminName_login);
		printf("请输入管理员密码:\n");
		scanf("%s",adminPW_login);
		if(strcmp(adminName,adminName_login)==0 && strcmp(adminPW,adminPW_login)==0){
			printf("管理员登录成功\n");
			break;
		}else{
			printf("账号或密码错误\n");
		}
	}

}

3.2. 查看所有用户

查看所有用户:管理员可以查看所有用户的基本信息,如账户卡号、姓名、手机号码、身份证号码、密码、余额等。

  1. 查看所用用户函数首先定义了一个 LinkNode 类型的指针 node,用于遍历链表中的节点,以及一个 Customer 类型的指针 user,用于存储用户的信息。
  2. 然后,函数定义了一个字符指针 oper,并将 "查看所有用户信息" 赋值给它,这将作为操作类型写入日志文件。
  3. 接下来,函数使用 nextNode 函数获取链表头节点的下一个节点,并将返回的节点指针赋值给 node
  4. 函数进入一个循环,直到 nodeNULL 时结束循环。
  5. 在循环中,函数首先为 user 分配内存,然后将 node 的数据部分转换为 DataType 类型,再将其 value 成员转换为 Customer 类型,并将结果赋值给 user
  6. 然后,函数使用 printf 函数打印出用户的账号开户时间、账户卡号、账户名称、手机号码、身份证、密码和账户金额。
  7. 最后,函数使用 nextNode 函数获取 node 的下一个节点,并将返回的节点指针赋值给 node
  8. 循环结束后,函数调用 adminLogsRecords 函数,将管理员的账号、密码和操作类型写入日志文件。
// 查看所有用户
void showAllUser(){
	// 直接打印linknode即可
	LinkNode *node;
	char *oper = "查看所有用户信息";  // 登录操作,写入日志文件
	Customer* user;
	node = nextNode(head);
	printf("所有用户信息:\n");
	while(node){
		user = (Customer*)malloc(sizeof(Customer));
		user = ((Customer*)((DataType*)node->data)->value);
		printf("账号开户时间:%s,账户卡号:%d,账户名称:%s,手机号码:%s,身份证:%s,密码:%s,账户金额:%f\n",user->Time,user->accountCard,user->accountName,user->mobile,user->sfz,user->password,user->money);
		node = nextNode(node);
	}
	adminLogsRecords(adminName,adminPW,oper);
}

3.3. 查看单个用户信息

查看单个用户信息:管理员可以输入一个用户的账户卡号,查看该用户的详细信息,以及该用户的操作记录。

  1. 函数首先定义了一个整型变量 accountCard,用于存储要查看的用户的银行账号,以及一个字符指针 oper,并将 "查看单个用户信息" 赋值给它,这将作为操作类型写入日志文件。
  2. 然后,函数为 Customer 类型的指针 user 分配内存,用于存储用户的信息。
  3. 接下来,函数使用 printf 函数提示输入要查看的用户的银行账号,并使用 scanf 函数读取输入的账号,存储到 accountCard 中。
  4. 函数进入一个无限循环,直到找到用户或者用户选择退出时才会结束循环。
  5. 在循环中,函数首先使用 getData 函数尝试从哈希表 hashmap 中获取 accountCard 对应的用户信息。
  6. 如果获取到的用户信息不为 NULL,那么函数将用户信息赋值给 user,并使用 printf 函数打印出用户的账户卡号、账户名称、手机号码、身份证、密码和账户金额,然后通过 break 语句跳出循环。
  7. 如果获取到的用户信息为 NULL,那么函数使用 printf 函数打印出 "抱歉,你查找的用户不存在。请继续操作" 的信息,并继续下一轮的循环,重新输入账号。
  8. 循环结束后,函数调用 adminLogsRecords 函数,将管理员的账号、密码和操作类型写入日志文件。
// 查看单个用户的信息和操作记录
void showSignalUser(){
	int accountCard; // 账户卡号
	char *oper = "查看单个用户信息";
	Customer* user = (Customer*)malloc(sizeof(Customer));
	printf("请输入您要查看用户信息的银行账号:\n");
	scanf("%d",&accountCard);

	while(1){
		if((Customer*)getData(hashmap,accountCard) != NULL){
			user = (Customer*)getData(hashmap,accountCard);
			printf("您需要查看的账户卡号 %d的用户信息是:账户名称:%s,手机号码:%s,身份证:%s,密码:%s,账户金额:%f\n",user->accountCard,user->accountName,user->mobile,user->sfz,user->password,user->money);
			break;
		}else{
			printf("抱歉,你查找的用户不存在。请继续操作\n");
			continue;
		}
	}
	adminLogsRecords(adminName,adminPW,oper);
}

3.4. 查看银行总体金额变化

查看银行总体金额变化:管理员可以查看银行的总资产,以及最近的资产变动记录,包括存款、取款、转账等操作的总金额。

  1. 函数首先定义了一个 FILE 类型的指针 fp,用于操作文件,以及一个字符数组 time,一个双精度浮点数 moneyBankTotalMoney,一个字符数组 oper,和一个整型变量 accountCard
  2. 然后,函数使用 fopen 函数以读取模式打开名为 "银行总金额变化文件.txt" 的文件,并将返回的文件指针赋值给 fp
  3. 如果文件打开失败,函数将打印出错误信息,并暂停程序,然后退出程序。
  4. 接下来,函数使用 printf 函数打印出 "银行银行整体金额变化:" 的信息。
  5. 函数进入一个循环,直到文件结束时结束循环。
  6. 在循环中,函数使用 fscanf 函数从文件中读取时间、账户卡号、操作类型、操作的金额和银行总体金额,并分别存储到 timeaccountCardopermoneyBankTotalMoney 中。
  7. 然后,函数使用 printf 函数打印出时间、账户卡号、操作类型、操作的金额和银行总体金额。
  8. 循环结束后,函数使用 fclose 函数关闭文件。
void showBankMoney() {
    FILE *fp;
	char time[30];
	double money = 0.0; // 操作的金额
	double BankTotalMoney = 0.0; // 银行总体金额
	char oper[20];
    int accountCard;

    fp = fopen("银行总金额变化文件.txt", "r");
    if (fp == NULL) {
        perror("打开文件失败啦");
        system("pause");
        exit(1);
    }
	printf("银行银行整体金额变化:\n");
    while (!feof(fp)) {
		fscanf(fp,"%s %d %s %lf %lf\n",&time,&accountCard,&oper,&money,&BankTotalMoney);
		printf("%s %d %s %lf %lf\n",time,accountCard,oper,money,BankTotalMoney);
	}
    fclose(fp);
}

3.5. 帮助用户修改密码

助用户修改密码:管理员可以输入一个用户的账户卡号,为该用户重置密码,以便于用户忘记密码时找回账户。

  1. 函数首先定义了一个整型变量 accountCard,用于存储要修改密码的用户的银行账号,以及三个字符数组 originPWpasswordpassword_confirm,分别用于存储原密码、新密码和确认新密码。
  2. 然后,函数定义了一个字符指针 oper,并将 "帮助用户修改密码" 赋值给它,这将作为操作类型写入日志文件。
  3. 接下来,函数为 Customer 类型的指针 user 分配内存,用于存储用户的信息。
  4. 函数进入一个无限循环,直到密码修改成功或者用户选择退出时才会结束循环。
  5. 在循环中,函数首先使用 printfscanf 函数提示输入要修改密码的用户的银行账号和原密码,并分别存储到 accountCardoriginPW 中。
  6. 然后,函数使用 getData 函数从哈希表 hashmap 中获取 accountCard 对应的用户信息,并将返回的用户信息赋值给 user
  7. 如果获取到的用户信息不为 NULL,那么函数使用 strcmp 函数比较 user 的密码和 originPW 是否相同。
  8. 如果密码相同,那么函数使用 printfscanf 函数提示输入新密码和确认新密码,并分别存储到 passwordpassword_confirm 中。
  9. 然后,函数使用 strcmp 函数比较 user 的密码和 password_confirm 是否相同,以及 passwordpassword_confirm 是否相同。
  10. 如果 user 的密码和 password_confirm 不相同,并且 passwordpassword_confirm 相同,那么函数使用 printf 函数打印出 "两次输入密码一致" 的信息,并通过 break 语句跳出循环。
  11. 如果 user 的密码和 password_confirm 相同,那么函数使用 printf 函数打印出 "修改密码不能与原密码一致!" 的信息,并继续下一轮的循环,重新输入新密码和确认新密码。
  12. 如果 passwordpassword_confirm 不相同,那么函数使用 printf 函数打印出 "两次输入的密码不一致,请继续操作" 的信息,并继续下一轮的循环,重新输入新密码和确认新密码。
  13. 如果 user 的密码和 originPW 不相同,那么函数使用 printf 函数打印出 "该用户密码输入错误!" 的信息,并继续下一轮的循环,重新输入银行账号和原密码。
  14. 如果获取到的用户信息为 NULL,那么函数使用 printf 函数打印出 "抱歉,你查找的用户不存在。请继续操作" 的信息,并继续下一轮的循环,重新输入银行账号。
  15. 循环结束后,函数使用 strcpy 函数将 password_confirm 复制到 user 的密码中,然后使用 printf 函数打印出 "密码修改成功!" 的信息。
  16. 最后,函数调用 userUpgrade_filePutadminLogsRecords 函数,将用户信息写入文件,并将管理员的账号、密码和操作类型写入日志文件。
void assistUserChangePW(){
	int accountCard; // 账户卡号
	char originPW[7]; // 账号原密码
	char password[7],password_confirm[7]; // 修改密码和修改密码的确认密码
	char *oper = "帮助用户修改密码";
	Customer* user = (Customer*)malloc(sizeof(Customer));
	while(1){
		printf("请输入您要修改密码的银行账号:\n");
		scanf("%d",&accountCard);
		printf("请输入您要修改密码的原密码:\n");
		scanf("%s",&originPW);
		user = (Customer*)getData(hashmap,accountCard);
		if(user != NULL){
			if(strcmp(user->password,originPW) == 0){
				printf("账号密码匹配成功!\n");
				printf("请输入您需要修改的密码\n");
				scanf("%s",&password);
				printf("请再次输入您需要修改的密码\n");
				scanf("%s",&password_confirm);
				if(strcmp(user->password,password_confirm) != 0){
					if(strcmp(password_confirm,password) == 0){
						printf("两次输入密码一致\n");
						break;
					}else{
						printf("两次输入的密码不一致,请继续操作");
						continue;
					}
				}else{
					printf("修改密码不能与原密码一致!\n");
					continue;
				}
			}else{
				printf("该用户密码输入错误!\n");
				continue;
			}
		}else{
			printf("抱歉,你查找的用户不存在。请继续操作\n");
			continue;
			}
	}
	strcpy(user->password,password_confirm); // user已经指向了hashmap,为什么不能直接修改
	printf("密码修改成功!\n");
	userUpgrade_filePut(head);
	adminLogsRecords(adminName,adminPW,oper);
	return;
}

3.6. 管理员日志记录

管理员日志记录:管理员的每一次操作都会被记录到一个文件中,包括操作时间、操作类型等信息,方便管理员查看自己的操作历史。

  1. 函数首先定义了一个 FILE 类型的指针 fp,用于操作文件,以及一个字符数组 time,用于存储当前时间。
  2. 然后,函数使用 fopen 函数以追加模式打开名为 "管理员操作日志文件.txt" 的文件,并将返回的文件指针赋值给 fp
  3. 接下来,函数调用 currentTime 函数获取当前时间,并将返回的时间字符串存储在 time 中。
  4. 函数使用 fprintf 函数将当前时间、管理员的账号、密码和操作类型写入文件。
  5. 最后,函数使用 fclose 函数关闭文件。
// 将管理员操作日志文件存入文件中
static void adminLogsRecords(char *adminName,char *adminPW,char *oper){
	FILE *fp;
	char time[30];
	fp = fopen("管理员操作日志文件.txt","a");
	currentTime(time);
	fprintf(fp,"%s %s %s %s\n",time,adminName,adminPW,oper);
	fclose(fp);
	return;
}

4. 后续

代码是我复习C语言时做的一个小的ATM系统,代码在我个人的测试下,还比较健壮,但是肯定还会存在一些隐藏的bug等待修复,代码中还存在一些可以优化的地方,之后有时间会去改进,或者大家有需求也可以提,鞭策我去优化和修bug。

对了,代码放在github,项目地址是:github.com/lpdbz/ATM_s…

觉得有帮助的同学,可以点一个免费的start鼓励一下呀,之后有时间优化ATM代码也会更新的此仓库的。我写的项目代码也都会放在自己的仓库。