【C语言】网吧管理系统-链表项目设计

这篇文章的一些修正已经更新:修正版

文章目录

  • C语言 & 网吧管理系统
    • 总起. 功能以及代码排版要求
    • 7. 系统
      • 7.1 主函数设计~
      • 7.2 开启系统后登录
        • 7.2.1 输入
        • 7.2.2 初始化+导入
        • 7.2.3 遍历链表~
      • 7.3 登录后操作
        • 7.3.1 ※函数指针数组(C进阶语法)
        • 7.3.2 登录成功显示
        • 7.3.3 选择操作环节
      • 7.4 关闭系统~exitOut
        • 7.4.1 更新管理员链表
        • 7.4.2 退出操作文件 - Exit.c
      • 7.5 测试
    • 6. 权限管理~
      • 6.1 主体函数设计
      • 6.2 菜单(Menu.c)
        • 6.2.1 菜单文件 - Menu.c
      • 6.3 删除管理员函数
      • 6.4 增加管理员 + 权限设置
        • 6.4.1 找到尾巴节点进行尾插~~
        • 6.4.2 设置权限
      • 6.5 退出操作
      • 6.5 测试
    • 2. 计费标准~
      • 2.1 计费标准结构体
      • 2.2 主体函数设计
        • 2.2.1 函数指针数组~
        • 2.2.2 文件读取函数文件-Carry.c
      • 2.3 菜单(Menu.c)
      • 2.4 新增计费标准
      • 2.5 查询计费标准
      • 2.6 删除计费标准
      • 2.7 修改计费标准
      • 2.8 退出操作
      • 2.9 测试
    • 1. 卡管理~
      • 1.1 卡这个整体对应的结构体~
      • 1.2 主体函数设计
        • 1.2.1 函数指针数组
        • 1.2.2 文件读取函数~
      • 1.3 菜单(Menu.c)
      • 1.4 添加卡
        • 1.4.1 对于卡数为0 的情况
      • 1.5 查询卡
        • 1.5.1 打印一个
          • 1.5.1.1 通过卡的类型打印对应的名字
        • 1.5.2 打印全部
      • 1.6 注销卡~
        • 1.6.1 确认注销
      • 1.7 退出操作
      • 1.8 测试
    • 4. 费用管理~
      • 4.1 主体函数设计
      • 4.2 充值
      • 4.3 退费
      • 4.4 退出操作
      • 4.5 测试
    • 3. 计费管理
      • 3.1 两个记录性结构体~
        • 3.1.1 Consume 消费记录(basis.h)
        • 3.1.2 Puncher 上机记录(basis.h)
      • 3.2 主体函数设计~
        • 3.2.1 读取文件操作
          • 3.2.1.1 consumeCarry(Carry.c)
          • 3.2.1.2 puncherCarry(Carry.c)
      • 3.3 菜单
      • 3.4 上机
        • 3.4.1 上机记录函数clockIn(ChargeManage.c)
      • 3.5 下机
        • 3.5.1 transfer(计费标准转化秒数函数 ChargeManage.c)
        • 3.5.2 局部变量:gap
        • 3.5.3 下机消费记录函数settlement(ChargeManage.c)
      • 3.6 退出操作
        • 3.6.1 消费单表的更新
        • 3.6.2 上机记录数据表
      • 3.7 补充:注销导致的强制下机计费
        • 3.7.1 强制下机消费记录函数 off (CardManage.c)
      • 3.8 测试
    • 5. 查询统计
      • 5.1 主体函数设计
        • 5.1.1 函数指针数组
      • 5.2 菜单
      • 5.3 查询单一卡一段时间的消费~
        • 5.3.1 细讲判断时间段方法~
      • 5.4 查询所有卡一段时间的总消费
      • 5.5 查询统计近一年来每个月的营销额以及上机次数~
        • 5.5.1 引入 Date 年月结构体(basis.h)
        • 5.5.2 引入全局变量:润平年月份日数对应数组~
        • 5.5.3 判断闰年函数以及判断年月函数~
        • 5.5.4 推导近12个月
        • 5.5.5 建立文本文档记录 + 打印到屏幕~
        • 5.5.6 遍历12次打印表格~
      • 5.6 退出操作
      • 5.7 测试~

C语言 & 网吧管理系统

  • 本文章是我(东莞理工学院)对于学校期末程序设计项目的总结,可以借鉴,请勿抄袭~
    • 数据结构:顺序表,链表
    • 知识点:C基础语法,C进阶语法,C文件操作
    • 内容简单易懂,难点主要在于项目代码思想思路设计,此系统可能有所缺陷,希望大家能提供建议!我特别需要~
    • 后附上源码,望大家支持~
  • 本文章可以很好的训练C语言语法,也很好的提高工程思维~

总起. 功能以及代码排版要求

重要前提: 你在下面学习的过程将可能会比较困惑,但是很大可能结合其后面所讲的内容,可以解决你的问题! 耐心耐心耐心

  • 多数存储方式应使用链式存储!
    • 这是学校的要求,不过我认为有些可以用到不同的数据结构来存储,例如适合查询的哈希表~
  • 每个功能都有单独的函数—这也是很重要的习惯~
    • 函数命名我采用小驼峰【就是单词与单词之间,用大写字母分割】
  • 对于每个模块,放在不用的源文件,好做区分,而申明均放在一个头文件中
    • 只需要包含这个头文件,就可以用里面的东西啦~
    • 函数调用的话,会跳转到特定的源文件,所以有一些函数不需要申明,加上static也不错~
      • 这里static有点像Java的private~
    • 我这个思想习惯于Java的类
      • 包括对应的链表的节点数,我是用一个全局变量代替
        • 不能用static修饰(包括函数也不行),因为申明了也不能给别的源文件使用~
        • 即使不申明,也不可以重名~
      • 不像Java面向对象,C语言这里还是很难像Java那样方便

【C语言】网吧管理系统-链表项目设计_第1张图片

序号 模块 功能 说明
1.1 添加卡 输入卡号、密码、开卡金额等卡信息,将卡信息保存到data文件夹的card.txt文件中
1.2 卡管理 查询卡 根据输入的卡号,查询卡号相同的卡信息,并以表格形式显示在控制台中
1.3 注销卡 根据输入的卡号、密码,将对应卡的状态改为注销,注销卡不能进行上机
2.1 新增计费标准 输入计费标准的信息,将输入的计费标准保存到data文件夹的rate.txt文件中
2.2 计费标准管理 查询计费标准 根据上机时间,查询对应的计费标准
2.3 删除计费标准 从计费标准文件data文件夹的rate.txt文件中,删除一条计费标准
2.4 修改计费标准 修改一条计费标准
3.1 计费管理 上机 根据输入的卡号、密码,判断该卡能否上机,如果能上机,则保存计费信息
3.2 下机 根据输入下机卡的卡号,进行下机结算操作
4.1 费用管理 充值 给一条已经存在的卡进行充值。
4.2 退费 将卡中余额返回给用户
5.1 查询消费记录 查询一张卡在一段时间内的消费记录
5.2 查询统计 统计总营业额 统计一段时间内,总营业额
5.3 统计月营业额 统计一年中,每个月上机次数、总营业额,将统计结果保存到文本文件中,并以列表形式显示在控制台中。
6.1 添加管理员 超级管理员添加一个管理员信息
6.2 权限管理 删除管理员 超级管理员删除一个管理员信息
6.3 配置权限 添加管理员时,配置管理员的权限
7.1 系统 登录 超级管理员和管理员登录系统
7.2 退出 超级管理员和管理员退出系统

  • 特别强调的是:表格的顺序并不是我们实际实现的顺序,要合理分析~
    • 比如,先有什么才有什么,按逻辑才是最重要的~
    • 下面我将细致地一步一步讲解~
    • 我的讲解方式是,先说思路和给源文件全部源码,再细分去一个个分析~
      • 例如,给全部—>源文件—>具体函数—>代码块~,而头文件,只需要需要的时候补充就好了
      • 总分分分分的形式~
      • 所以一旦发现陌生的东西都不要担心哦,后面都会细说的~
      • 并且我们可以假设已经写好所有的函数,定好大致框架~
    • 源码在这里:网吧管理系统 · 游离态
    • 总的思维导图在这里捏:网吧管理系统/data/网络管理系统.xmind · 游离态
    • 后续讲得比较分散,可能会导致哪些东西不知道要放哪,所以你可以尝试看一看全部的源码~
      【C语言】网吧管理系统-链表项目设计_第2张图片

7. 系统

开启系统:

  • 登录
    • 进入系统后登录~
    • 确定身份后登录后进行其他操作,才有登出操作
    • 登出后可以选择继续登录
  • 关闭系统
    • 不选择继续登录,可选择是否再次开启系统~

退出选择:

  • 程序的终结~
    • 真正意义上的完全退出~
      【C语言】网吧管理系统-链表项目设计_第3张图片

7.1 主函数设计~

  • 建立一个源文件test.c,头文件basis.h

    • 源码在这里 : 网吧管理系统/test.c · 游离态 网吧管理系统/basis.h · 游离态
  • 此时在头文件已有信息:

    • 重点说明:不同源文件的信息只需要在头文件申明就好,因为每个源文件都包含了这个basis.h头文件~

      • 并且会在后方填入写在哪个文件,并添加函数是在调用者函数的上方~
    • #pragma once//防止重复包含头文件
      
      //不要在这个文件开始运行程序~~~会报错!
      //并且上方列表如无出现test.c,也会报错
      #define _CRT_SECURE_NO_WARNINGS 1
      
      //系统头文件
      #include
      #include //动态内存分配函数~
      
      void menu();
      
  • 此时在源文件test.c已有信息:

    • #include "basis.h"//用双引号:第一步在本目录下查找头文件,第二步,在系统中查找头文件
      				 //系统头文件一般用< >,一步到位直接在系统中查找头文件~~
      int main() {
      	int input = 0;
      	do {
      		menu();
      		scanf("%d", &input);
      		switch (input) {
      			case 1:
      				signIn();//选择开启系统进入登录页面~~~
      				break;
      			case 0:
      				printf("退出成功\n");//退出减少简单的退出,程序彻底的结束了~
      				break;
      			default:
      				printf("请重新输入\n");//输入错误了~
      				break;
      		}
      	} while (input);
      
      	return 0;
      }
      
    • 这里用到了常见的多次选择算法~

    • 实现每次从关闭系统之后选择是否继续开启系统~

  • 建立一个源文件Menu.c,存放所有的菜单~

    • 现在已有信息:

    • void menu() {
      	printf("***************\n");
      	printf("1. 管理员登录 \n");
      	printf("0. 退出\n");
      	printf("***************\n");
      }
      

7.2 开启系统后登录

  • signIn()函数
void signIn() {
	printf("请输入编号,姓名以及六位密码:>");
	int id = 0;
	char name[10] = { 0 };
	char password[7] = { 0 };
	int retscan = scanf("%d%s%s", &id, name, password);
	if (retscan == 0) {
		printf("\n由于输入错误,系统崩坏\n");
		exit(0);
	}
	//错误输入会死循环的原因在于,格式化输入错误
    
   //----------------------------- 
	FILE* pf = fopen("data\\manager.txt", "rb");
	if (pf == NULL) {
		init();
		pf = fopen("data\\manager.txt", "rb");
	}
	fread(&managerNumber, sizeof(int), 1, pf);
	if (managerNumber == 0) {
		fclose(pf);
		init();
		pf = fopen("data\\manager.txt", "rb");
	}
	Manager* pm = (Manager*)malloc(sizeof(Manager));
	fread(pm, sizeof(Manager), 1, pf);
	Manager* cur = pm;
	head = pm;
	for (int i = 1; i < managerNumber; i++) {
		Manager* newOne = (Manager*)malloc(sizeof(Manager));
		fread(newOne, sizeof(Manager), 1, pf);
		newOne->next = NULL;
		cur->next = newOne;
		cur = cur->next;
	}
	fclose(pf);
    
   //----------------------------- 
	cur = pm;
	while (cur != NULL) {
		if (id == cur->id
			&& strcmp(cur->name, name) == 0
			&& strcmp(cur->password, password) == 0) {
			letUsGo(cur);
			return;
		}
		cur = cur->next;
	}
	printf("您暂且不是管理员或者您的卡号密码错误\n");//能到这里必然就是登录失败的~
}

不用担心, 下面是细节分析!

7.2.1 输入

	printf("请输入编号,姓名以及六位密码:>");
	int id = 0;//编号
	char name[10] = { 0 };//姓名
	char password[7] = { 0 };//密码
	int retscan = scanf("%d%s%s", &id, name, password);//返回值为正常输入的元素个数~
//如果输入失败,里面退出
	if (retscan == 0) {
		printf("\n由于输入错误,系统崩坏\n");
		exit(0);//库函数,exit(0)代表安全退出~
	}
	//错误输入会死循环的原因在于,格式化输入错误

7.2.2 初始化+导入

  • 引入结构体Manager(basis.h)

  • //管理员,typedef -- 换名字~
    typedef struct Manager {
    	int id;//编号
    	char name[10];//名字
    	char password[7];//密码
    	char limits[8];//权限
    	struct Manager* next;//后驱
    }Manager;
    
    • 重点讲解权限:有7种权限,1代表有权限,0代表无权限
	FILE* pf = fopen("data\\manager.txt", "rb");
	if (pf == NULL || managerNumber == 0) {
		init();//初始化函数
		pf = fopen("data\\manager.txt", "rb");//要重写打开哦~
	}


	fread(&managerNumber, sizeof(int), 1, pf);
	Manager* pm = (Manager*)malloc(sizeof(Manager));
	fread(pm, sizeof(Manager), 1, pf);
	Manager* cur = pm;
	head = pm;
	for (int i = 1; i < managerNumber; i++) {
		Manager* newOne = (Manager*)malloc(sizeof(Manager));
		fread(newOne, sizeof(Manager), 1, pf);
		newOne->next = NULL;
		cur->next = newOne;
		cur = cur->next;
	}
	fclose(pf);
  • 对于读取失败或者管理员的个数为0的情况下,进行初始化

    • 引入全局变量:managerNumber head

      • managerNumber(test.c)

      • 计算已有管理员的个数

        • 放在源文件中,然后头文件声明

        • int managerNumber = 0;//源文件中
          
        • extern managerNUmber;
          
      • head(basis.h)(必须放在Manager结构体定义之后)

      • 存储已有管理员,作为头结点代表整条链表

      • 方便后续释放空间以及更新管理员信息二进制文件~

        • 放在头文件中(指针应该放在头文件),原因不做解释

        • Manager* head;
          
    • 初始化函数(test.c)

      • 重点说明:每一个二进制文件的前四个字节,我默认放的都是元素个数 !

      • *建立存放manager信息的二进制文件~==》*manager.txt

      • 据学校要求,放置在data文件中,必须提前建立~

      • malloc库函数~ 申请堆区空间,函数栈帧销毁此~

      • 字符串拷贝用strcpy库函数

        • 可以记录一下管理员表格,防止忘记了~
        • 位置:网吧管理系统/data/备忘录.xlsx · 游离态
                  void init() {
          	FILE* pfr = fopen("data\\manager.txt", "wb");//自动建立二进制文件
              //选取二进制文件是因为读取写入最方便安全~~~
          	Manager* pm1 = (Manager*)malloc(sizeof(Manager));
          	Manager* pm2 = (Manager*)malloc(sizeof(Manager));
          	Manager* pm3 = (Manager*)malloc(sizeof(Manager));
          	Manager* pm4 = (Manager*)malloc(sizeof(Manager));
          	Manager* pm5 = (Manager*)malloc(sizeof(Manager));
          	Manager* pm6 = (Manager*)malloc(sizeof(Manager));
          	pm1->id = 0;
          	strcpy(pm1->name, "小马");
          	strcpy(pm1->password, "123456");
          	strcpy(pm1->limits, "1111111");
          	pm2->id = -1;
          	strcpy(pm2->name, "小张");
          	strcpy(pm2->password, "123456");
          	strcpy(pm2->limits, "1111111");
          	pm3->id = -2;
          	strcpy(pm3->name, "老师");
          	strcpy(pm3->password, "123456");
          	strcpy(pm3->limits, "1111111");
          	pm4->id = 1;
          	strcpy(pm4->name, "小卡拉");
          	strcpy(pm4->password, "123456");
          	strcpy(pm4->limits, "1110000");
          	pm5->id = 2;
          	strcpy(pm5->name, "小空多尼");
          	strcpy(pm5->password, "123456");
          	strcpy(pm5->limits, "1111110");
          	pm6->id = 3;
          	strcpy(pm6->name, "小林");
          	strcpy(pm6->password, "123456");
          	strcpy(pm6->limits, "1111110");
          	managerNumber = 6;
          	fwrite(&managerNumber, sizeof(int), 1, pfr);//给6个
          	fwrite(pm1, sizeof(Manager), 1, pfr);
          	fwrite(pm2, sizeof(Manager), 1, pfr);
          	fwrite(pm3, sizeof(Manager), 1, pfr);
          	fwrite(pm4, sizeof(Manager), 1, pfr);
          	fwrite(pm5, sizeof(Manager), 1, pfr);
          	fwrite(pm6, sizeof(Manager), 1, pfr);
          	fclose(pfr);
      }
      
    • 初始化默认加入三个超级管理员三个普通管理员~

    • 超级管理员编号小于等于0~普通管理员编号默认大于0~

      • 超级管理员权限全是1~

      • 存入文件,成员next 是没有价值的,下次读取这个地址也没用~

  • 对于读取成功,进行内容导入~

    1. 改变全局变量managerNumber

      	fread(&managerNumber, sizeof(int), 1, pf);
      
    2. 此时文件不可能没有内容,因为经过初始化了

      	Manager* pm = (Manager*)malloc(sizeof(Manager));
      	fread(pm, sizeof(Manager), 1, pf);
      	Manager* cur = pm;
      	head = pm;//用全局head -> 记录头结点地址~
      
    3. 读取文件并关闭文件~

      • 一个元素一个元素的读取,以尾插法延伸~
      	for (int i = 1; i < managerNumber; i++) {
      		Manager* newOne = (Manager*)malloc(sizeof(Manager));
      		fread(newOne, sizeof(Manager), 1, pf);
      		newOne->next = NULL;
      		cur->next = newOne;
      		cur = cur->next;
      	}
      	fclose(pf);
      

7.2.3 遍历链表~

  • 字符串之间比较用strcmp函数~
  • 包含头文件#include(basis.h)
	cur = pm;//回到链表首~
	while (cur != NULL) {
		if (id == cur->id
			&& strcmp(cur->name, name) == 0
			&& strcmp(cur->password, password) == 0) {
			letUsGo(cur);//登录成功后的操作~
			return;
		}
		cur = cur->next;//走向下一步~
	}
	printf("您暂且不是管理员或者您的卡号密码错误\n");//能到这里必然就是登录失败的~

7.3 登录后操作

  • 登录系统后预操作函数letUsGo()(test.c)
void letUsGo(Manager* pm) {
	int input = 0;
    
    //函数列表~
    //操作相关函数~
    //返回值参数列表都相同~
	void(*opera[7])(Manager*) =
	{ 
		exitOut, cardManage, 
		chargeStandard, chargeManage, 
		expenseManage, searchStatistics, 
		limitsManage 
	};
	printf("--------------------------\n");
	time_t t = time(NULL);
	printf("%s\n于%s登录系统\n", pm->name, ctime(&t));
	printf("--------------------------\n");
	do {
		systemMenu(pm);
		scanf("%d", &input);
		if (pm->limits[input] != '1') {
			printf("你并没有权限");
		}
		else {
			opera[input](pm);
			//管理员链表的free,在退出登录时free
		}
	} while (input);
}

7.3.1 ※函数指针数组(C进阶语法)

  • 对于一个函数而言,其函数名实际上就是一个函数指针,并且函数指针的地址还是本身~

  • 函数指针形式: 用(*p)去替换函数名位置

    • 例如void exitOut(Manager* pm) ===》void (*p)(Manager*)
  • 那么函数指针数组就是(*opera[])去替换函数名位置~

  • void (*opear[])(Manager*)

  • 这个数组中的内容都是同一类函数的函数名~

    • 参数列表以及返回值是相同的~
  • 用这个p指针可以直接调用这个函数

  • exitOut(pm) <=> opera[0](pm) <=> p0(pm)

  • 	//下标对应好哦!															
    	void(*opera[7])(Manager*) =
    	{ 
    		exitOut, cardManage, 
    		chargeStandard, chargeManage, 
    		expenseManage, searchStatistics, 
    		limitsManage 
    	};
    
  • 这些是后续的操作的函数名列表~

对于为什么用函数指针,是为了让代码更加简洁,并且提高运行速率(空间换时间);

7.3.2 登录成功显示

  • 包含一个头文件#include (basis.h)
    • 这个头文件里面含有时间函数~
    • 有我们所需要的函数:time()ctime()
      • 前者是获得当前时间戳【1970年1月1日 00:00:00到现在的秒数】
      • 后者是将时间戳转化为字符串(年月日时分秒)【英文~】
    • 首先,time(NULL) ==> 获取当前时间戳**(类型为time_t ,实际上是64位长整形)**
    • 其次,将地址传过去
      • 注意,不能写成&(time(NULL)),一个确切的值怎么可以被取地址?只有变量/常量可以
	printf("--------------------------\n");
	time_t t = time(NULL);
	printf("%s\n于%s登录系统\n", pm->name, ctime(&t));
	printf("--------------------------\n");

7.3.3 选择操作环节

  • 引入菜单systemMenu(pm)(Menu.c)

    • void systemMenu(Manager* pm) {
          //通过编号确认管理员类型~
      	if (pm->id <= 0) {
      		printf("你好,超级管理员,%s!\n", pm->name);
      	}
      	else {
      		printf("你好,普通管理员,%s!\n", pm->name);
      	}
          //根据权限判断~~
      	printf("******************\n");
      	if (pm->limits[0] == '1') {
      		printf("0. 退出系统\n");//所有人都有这个功能,要怎么设计呢,随后揭晓~
      	}
      	if (pm->limits[1] == '1') {
      		printf("1. 卡管理\n");
      	}
      	if (pm->limits[2] == '1') {
      		printf("2. 计费标准管理\n");
      	}
      	if (pm->limits[3] == '1') {
      		printf("3. 计费管理\n");
      	}
      	if (pm->limits[4] == '1') {
      		printf("4. 费用管理\n");
      	}
      	if (pm->limits[5] == '1') {
      		printf("5. 查询统计\n");
      	}
      	if (pm->limits[6] == '1' && pm->id <= 0) {
      		printf("6. 权限管理\n");
      	}
      	printf("******************\n");
      }
      
    • 只有有权限的内容才会显示!

  • 还是老样子~==》多次输入确认算法

  • 	int input = 0;
    	do {
    		systemMenu(pm);
    		scanf("%d", &input);
            //确保是否有权限(因为可能菜单没显示,他也选了~这不就是卡bug了吗)
    		if (pm->limits[input] != '1') {
    			printf("你并没有权限");
    		}
    		else {
    			opera[input](pm);//调用特定下标的函数~~~
    			//管理员链表的free,在退出登录时free,不能在退出权限设置的时候free,这样会导致pm被释放,后续无法操作
                //有个误区,就是每次操作的退出,都要对直接影响的链表进行保存释放~权限设置函数确实直接影响了管理员链表
                //但是不能在那个时候释放~
                //这里看不懂无所谓,等权限设置函数讲完之后在看
    		}
    	} while (input);
    
  • 选择0调用退出函数并结束循环~

7.4 关闭系统~exitOut

  • 引入void exitOut(Manager* pm)(Exit.c)

  • 即这里的opera[0]~

  • void exitOut(Manager* pm) {
    	time_t t = time(NULL);
    	printf("--------------------------\n");
    	printf("%s\n于%s退出系统\n", pm->name, ctime(&t));
    	printf("--------------------------\n");
    
    	exitOutManager();
        //在这里就可以将管理员链表进行更新了,因为这里代表了此管理员操作的终结,此管理员有可能新增或者删除过管理员~
    }
    
  • 退出的时候使用时间函数,报告时间,模拟实际情况~

  • 在这里退出,是因为这一步结束后,应该是别的管理员进入系统了,所以更新管理员链表,让新添加的管理员可以紧接着进入~

    • 后面会细讲~

7.4.1 更新管理员链表

  • exitOutManager(Exit.c)

  • void exitOutManager() {
    	printf("退出成功\n");
    	FILE* pf = fopen("data\\manager.txt", "wb");
    
    	fwrite(&managerNumber, sizeof(int), 1, pf);
    	//记录管理员名单
    	Manager* cur = head;
    	while (cur != NULL) {
    		fwrite(cur, sizeof(Manager), 1, pf);
    		Manager* tmp = cur;//通过寄存器做中介
    		cur = cur->next;
    		free(tmp);
    	}
    	//在这里一定要关闭,不然只会到程序结束,内容才会从缓存区传入文件!!!
    	fclose(pf);
    	pf = NULL;
    }
    
  1. 将管理员个数导入文件
  2. 从头开始遍历,将每一个节点导入文件
  3. 同时将每一部分空间进行释放~
    • 不释放,空间泄露可能会很危险,对于一些大工程而言
    • 所以要有这个习惯~

7.4.2 退出操作文件 - Exit.c

  • 我将所有退出操作放进一个源文件中
  • 方便管理~
  • 源码在这里:网吧管理系统/Exit.c · 游离态

7.5 测试

  • 代码运行正常~

【C语言】网吧管理系统-链表项目设计_第4张图片

  • 二进制文件显示正常~
    • 因为VS2022中,在内存中,数据存储是小端存储~
    • 小端存储就是,真值小的字节地址小,真值大的字节地址大
      • 大端就是反着来
    • 所以下图的06 00 00 00,8位十六进制数字,在内存中对应的int整形就是6~

【C语言】网吧管理系统-链表项目设计_第5张图片


6. 权限管理~

  • 不难想到这个基础菜单是因为有权限所以才看得到,这个操作应该要先实现~
  • 【C语言】网吧管理系统-链表项目设计_第6张图片

6.1 主体函数设计

  • 引出函数limitsManage()(LimitsManage.c)
  • 选项少,用switch( ) + do while
void limitsManage(Manager* pm) {
	int input = 0;
	do {
		menu6_1();//菜单
		scanf("%d", &input);
		switch (input) {
		case 0 : 
			printf("退出成功\n"); //退出就是简简单单的退出~
			break;
		case 1:
			add(pm);//增加函数
			break;
		case 2 : 
			delete(pm);//删除函数
			break;
		default :
			printf("输入错误\n");
			break;
		}
	} while (input);
}

6.2 菜单(Menu.c)

  • 选择增减 菜单~

    • void menu6_1() {
      	printf("******************\n");
      	printf("0. 退出此操作\n");
      	printf("1. 添加管理员\n");
      	printf("2. 删除管理员\n");
      	printf("******************\n");
      }
      
  • 权限设置 菜单~

    • void menu6_2(Manager* pm) {
      	printf("******************\n");
      	printf("1. 卡管理权限\n");
      	printf("2. 计费标准管理权限\n");
      	printf("3. 计费管理权限\n");
      	printf("4. 费用管理权限\n");
      	printf("5. 查询统计权限\n");
      	printf("0. 结束本次操作\n");
      	printf("******************\n");
      }
      

6.2.1 菜单文件 - Menu.c

  • 我将所有问菜单放在了一个源文件中,方便管理~
  • 源码在这里:/网吧管理系统/Menu.c · 游离态

6.3 删除管理员函数

  • delect(LimitsManage.c)

    • 由于超级管理员不能新增,并且被我设置在前面
    • 所以拥有此权限的人,必然是超级管理员
    • 得出删除的管理员必然在pm的后面~
  • 只需要用探路指针遍历就好~

  • void delete(Manager* pm) {
    	printf("请输入管理员的编号,姓名:>");
    	//由于此操作只能由超级管理员操作并且超级管理员在最前面,
    	//所以在这里往后遍历就好,但是不能删除超级管理员
    	int id = 0;
    	char name[10] = { 0 };
    	Manager* cur = pm;
    	do {
    		scanf("%d%s", &id, name);
    		if (id <= 0) {
    			printf("无法删除!\n");//这里绝对是不行的!!!因为超级管理员的编号<=0~
    		}
    	} while (id <= 0);
    	while (cur->next != NULL) {
    		if(id == cur->next->id && strcmp(name, cur->next->name) == 0){
    			Manager* rubbish = cur->next;
    			cur->next = rubbish->next;
    			free(rubbish);
    			printf("删除成功\n");
    			managerNumber--;
    			return;
    		}
    		cur = cur->next;
    	}
    	printf("此人并不是管理员\n");
    }
    
  1. 无法删除超级管理员~

  2. 输入编号姓名删除管理员

  3. 遍历链表查找(由于编号唯一,理当只删一个,不应该在添加管理员时使用相同编号,但是我这个项目不会查重

    • 找到了删完return

      • 这里的思路是:(删除节点操作)
        1. pm是本人,绝对不是被删除的,所以只需要判断后面的就行
        2. 每次都判断后驱的那个是不是要被删的
        3. 是的话,记录待被删除的节点(要free哦)
          1. Manager* rubbish = cur->next;
          2. cur->next = rubbish->next; 越过节点,完成删除~
          3. free(rubbish);
          4. managerNumber--; 管理员数量减一
    • 【C语言】网吧管理系统-链表项目设计_第7张图片

    • 找不到打印找不到的信息~

6.4 增加管理员 + 权限设置

  • 函数add (LimitsManage.c)
void add(Manager* pm) {
	printf("请输入新增管理员的编号,姓名,六位密码:>");
	Manager* cur = pm;
	while (cur->next != NULL) {
		cur = cur->next;
	}
	int id = 0;
	char name[10];
	char password[7];
	scanf("%d%s%s", &id, name, password);
	if (id <= 0) {
		printf("普通管理员的id理应大于0!\n");
		return;
	}
	else {
		cur->next = (Manager*)malloc(sizeof(Manager));
		cur = cur->next;
		cur->id = id;
		strcpy(cur->name, name);
		strcpy(cur->password, password);
        strcpy(cur->limits, "0000000");
		cur->next = NULL;
	}
	int input = 0;
	menu6_2(pm);
	printf("请输入要为其设置的权限:>");
	do {
		scanf("%d", &input);
		if (input <= 5 && input >= 0) {
			cur->limits[input] = '1';
		}
		else {
			printf("输入失败\n");
		}
	} while (input);
	printf("添加成功\n");
	managerNumber++;
}

6.4.1 找到尾巴节点进行尾插~~

  • 找到尾巴并设置基础属性,编号、姓名、六位密码~
	printf("请输入新增管理员的编号,姓名,六位密码:>");
	Manager* cur = pm;
	while (cur->next != NULL) {
		cur = cur->next;
	}
	int id = 0;
	char name[10];
	char password[7];
	scanf("%d%s%s", &id, name, password);
	if (id <= 0) {
		printf("普通管理员的id理应大于0!\n");
		return;
	}
	else {
		cur->next = (Manager*)malloc(sizeof(Manager));
		cur = cur->next;
		cur->id = id;
		strcpy(cur->name, name);
		strcpy(cur->password, password);
		strcpy(cur->limits, "0000000");//默认全为0~
        cur->next = NULL;
	}
  • 探路指针cur代替pm去跑到末尾~
  1. 判断id是否合理(不合理直接退出)
  2. 合理即添加

6.4.2 设置权限

  • 此时的cur指向的就是新增节点~

  • 通过多次输入选项,对对应的权限进行设置

    • 【C语言】网吧管理系统-链表项目设计_第8张图片
  • 按0的时候,会直接将0下标的那个权限设置为1,顺带因此退出了本次操作

  • 不能赋予增减管理员设置的权限~

  • managerNumber++;管理员数+1

	int input = 0;
	menu6_2(pm);//菜单~~~
	printf("请输入要为其设置的权限:>");
	do {
		scanf("%d", &input);
		if (input <= 5 && input >= 0) {
			cur->limits[input] = '1';
		}
		else {
			printf("输入失败\n");
		}
	} while (input);
	printf("添加成功\n");
	managerNumber++;

6.5 退出操作

  • 简简单单的退出~

※注意:管理员链表的更新应该在这个管理员退出系统的时候才能更新,否则此管理员被释放了,无法继续后面的操作!并且新增的管理员才能进入系统~

6.5 测试

【C语言】网吧管理系统-链表项目设计_第9张图片
【C语言】网吧管理系统-链表项目设计_第10张图片

  • 测试结果正常~

2. 计费标准~

  • 也很好理解,有了计费标准,才能有一张卡嘛~
  • 我的思路就是:分为年月日时四种卡,这样就固定了这个计费标准就一直都是四个
    • 时间较长的卡不应该太早下机~即使当天不玩,也可以不下机

新源文件ChargeStandard.c 源码在这里:网吧管理系统/ChargeStandard.c · 游离态

【C语言】网吧管理系统-链表项目设计_第11张图片

2.1 计费标准结构体

  • Standard(basis.h)
//计费标准
typedef struct Standard {
	int type;//1 2 3 4
	int state;//决定此卡是否能办
	double price;//标准单价
}Standard;
  1. type => 卡的类型~
  2. state=> 是否有此标准~
  3. price=>标准单价~
  • 规则
    • 有这个标准就不能新增,没有这个标准就不能删除和更改
    • 并且没有这个标准,就没有对应的卡,对应的卡被建立,此标准被删除,则必须补充标准才能正常下机!
    • 修改此标准,该卡将以最终标准进行计费~

2.2 主体函数设计

  • chargeStandard(ChargeStandard.c )
void chargeStandard(Manager* pm) {
	int input = 0;
	Standard* pcs = standardCarry();
	void (*func[5])(Standard*) = { exitOutStandard, addStandard, search, delStandard, modify};
	do {
		menu2_1();//菜单~
		scanf("%d", &input);
		if (input <= 4 && input >= 0) {
			func[input](pcs);
		}
		else {
			printf("请重新输入\n");
		}
	} while (input);
}

2.2.1 函数指针数组~

  • 这里由于选项较多,我选择用函数指针数组,用法与刚才一样,要注意下标与菜单与对应选项要相符合哦~

2.2.2 文件读取函数文件-Carry.c

  • 我将所有的结构体(信息)文件读取操作,都放在一个源文件中,方便管理~

  • 源码在这里:网吧管理系统/Carry.c · 游离态

这里的 Standard* pcs = standardCarry();的含义就是从文件中读取~

并且返回值为对应的首元素堆区地址

  • 信息放在rate.txt二进制文件中~
Standard* standardCarry() {
	FILE* pf = fopen("data\\rate.txt", "rb");
	Standard* pcs = (Standard*)malloc(4 * sizeof(Standard));
	if (pf == NULL) {
		pf = fopen("data\\rate.txt", "wb");
		fwrite(pcs, sizeof(Standard), 4, pf);
		fclose(pf);
	}
	else {
		fread(pcs, sizeof(Standard), 4, pf);
		fclose(pf);
	}
	return pcs;
}
  1. 标准的个数一直都是4个,顺序一定1 2 3 4,只不过state可能有所不同,所以我用的是顺序表
  2. 打开文件失败则必然是不存在此文件,则应该进行判断~

2.3 菜单(Menu.c)

  • 增删查改操作菜单:

    • void menu2_1() {
      	printf("******************\n");
      	printf("0. 退出\n");
      	printf("1. 新增计费标准\n");
      	printf("2. 查询计费标准\n");
      	printf("3. 删除计费标准\n");
      	printf("4. 修改计费标准\n");
      	printf("******************\n");
      }
      
  • 卡的类型清单:

    • void menu2_2() {
      	printf("******************\n");
      	printf("1. 年卡\n");
      	printf("2. 月卡\n");
      	printf("3. 日卡\n");
      	printf("4. 时卡\n");
      	printf("******************\n");
      }
      

2.4 新增计费标准

addStandard(ChargeStandard.c)

  • 选择卡的类型~
  • 不存在此标准则可增加
    • 输入标准信息~
  • pcs[input - 1].state = 1;
    • 下标访问其实就是解引用操作~
      • arr[i] == *(arr + i)
      • 所以下标为负在C语言里也不会报错~
    • state置为1—>已有标准~
void addStandard(Standard* pcs) {
	int input = 0;
	menu2_2();
	printf("请输入待增加的计费标准的类型:>");
	scanf("%d", &input);
	if (input <= 4 && input >= 1 && pcs[input - 1].state != 1) {
		printf("请输入你的计费标准单价是多少元:>");
		scanf("%lf", &pcs[input - 1].price);
		pcs[input - 1].state = 1;
		printf("新增成功\n");
	}
	else {
		printf("此标准无法加入,“可能”是已有此标准,但可进行修改操作\n");
	}
}

2.5 查询计费标准

search(ChargeStandard.c)

  • 选择卡的类型~
  • 打印卡的信息~
    • 无此卡则报无~
void search(Standard* pcs) {
	int input = 0;
	menu2_2();
	printf("请输入待查看的计费标准的类型:>");
	scanf("%d", &input);
	if (input <= 4 && input >= 1 && pcs[input - 1].state == 1) {
		printf("计费标准为:"); 
		change(input);
		printf("=》单位时间内收费%.2lf元\n", pcs[input - 1].price);
	}
	else {
		printf("暂无此标准\n");
	}
}

2.6 删除计费标准

delStandard(ChargeStandard.c)

  • 选择卡的类型

  • 存在此标准,就删除

    • pcs[input - 1].state = 0;
    • state 置为0 —> 无标准
  • 不存在就报失败~

void delStandard(Standard* pcs) {
	int input = 0;
	menu2_2();
	printf("请输入待删除计费标准的类型:>");
	scanf("%d", &input);
	if (input <= 4 && input >= 1 && pcs[input - 1].state == 1) {
		pcs[input - 1].state = 0;
		printf("删除成功\n");
	}
	else {
		printf("删除失败\n");
	}
}

2.7 修改计费标准

modify(ChargeStandard.c)

  • 选择卡的类型
  • 存在此标准,输入对应信息进行修改~
  • 不存在则报无~
void modify(Standard* pcs) {
	int input = 0;
	menu2_2();
	printf("请输入待修改计费标准的类型:>");
	scanf("%d", &input);
	if (input <= 4 && input >= 1 && pcs[input - 1].state == 1) {
		printf("请输入你的调整后的计费标准单价是多少元:>");
		scanf("%lf", &pcs[input - 1].price);
		printf("修改成功\n");
	}
	else {
		printf("暂无此标准\n");
	}	
}

2.8 退出操作

exitOutStandard(Exit.c)

  • func[0]
void exitOutStandard(Standard* pcs) {
	FILE* pf = fopen("data\\rate.txt", "wb");
	fwrite(pcs, sizeof(Standard), 4, pf);
	free(pcs);
	fclose(pf);
}
  • 释放空间,更新信息~

2.9 测试

  • 下面的测试案例,都是事先被我插入数据的~

  • 运行正常~
    【C语言】网吧管理系统-链表项目设计_第12张图片

  • 二进制文件显示正常~
    【C语言】网吧管理系统-链表项目设计_第13张图片


1. 卡管理~

  • 有了计费标准之后,我们基本可以开始构建卡了~
  • 引入新的源文件:CardManage.c
  • 源码在这里:网吧管理系统/CardManage.c · 游离态
  • 【C语言】网吧管理系统-链表项目设计_第14张图片

1.1 卡这个整体对应的结构体~

  • Card(basis.h)
//卡
typedef struct Card {
	int id;//卡号
	char password[7];//六位密码
	double balance;//开卡金额-->余额,卡的种类决定了这个金额
	int effect; // 1-->未注销,非1-->已注销
	int cardType;//卡的种类---计费方案
	long long upTime;//上机间点
	int state;//上机与否?
	struct Card* next;//后继
}Card;
  1. 卡号

  2. 密码

  3. 余额

  4. 效应—注销与否

  5. 卡的类型—计费标准

  6. 上机的时间的时间戳~(64位长整形)

  7. 状态—上机了与否

  8. 后继节点—构造链表

1.2 主体函数设计

cardManage(CardManage.c)

  • 引入新的全局变量:cardNumber(CardManager.c)卡数~
void cardManage(Manager* pm) {
	int input = 0;
	Card* pc = cardCarry();
	void (*func[4])(Card*) = 
	{ exitOutCard, addCard, searchCard, logOffCard };
	do {
		menu1_1();
		scanf("%d", &input);
		if (input >= 0 && input <= 3) {
			func[input](pc);
		}
		else {
			printf("输入失败\n");
		}
	} while (input);

}

1.2.1 函数指针数组

  • 同样的,选项大于大于3个我就认为有点多了

1.2.2 文件读取函数~

cardCarry(Carry.c)

Card* cardCarry() {
	FILE* pf = fopen("data\\card.txt", "rb");
	Card* pc = (Card*)malloc(sizeof(Card));//堆区空间,不会被收回
	if (pf == NULL) {
		pf = fopen("data\\card.txt", "wb");
		fwrite(&cardNumber, sizeof(int), 1, pf);
		fclose(pf);
	}
	else {
		fread(&cardNumber, sizeof(int), 1, pf);
		fread(pc, sizeof(Card), 1, pf);
		Card* cur = pc;
		for (int i = 1; i < cardNumber; i++) {
			Card* newOne = (Card*)malloc(sizeof(Card));
			fread(newOne, sizeof(Card), 1, pf);
			newOne->next = NULL;
			cur->next = newOne;
			cur = cur->next;
		}
	}
	return pc;
}
  1. 文件指针为NULL代表打不开文件,说明文件不存在
    • 则我只需要建立一个,并且将整数0导入文件中~
    • 我的一个习惯:我会讲元素个数放在文件的首位~
      • 刚才的计费标准没有是因为一直都是4个~
  2. 文件打开了,进行标准的读取操作
    • 首先拿到卡数~
    • 然后获得第一张卡~
    • 后续以尾插的形式进行插入~
  3. 重点:要将尾节点的后驱置为NULL,否则会导致野指针异常/死循环~

※注意:如果文件中没有东西,这pc指针中堆区空间仍然是原始的~后续应该通过卡的个数对此情况进行处理!!!

1.3 菜单(Menu.c)

  • 添查销操作菜单

    • void menu1_1() {
      	printf("******************\n");
      	printf("1. 添加卡\n");
      	printf("2. 查询卡\n");
      	printf("3. 注销卡\n");
      	printf("0. 退出\n");
      	printf("******************\n");
      }
      
  • 查询操作选项菜单

    • void menu1_2() {
      	printf("******************\n");
      	printf("1. 打印全部卡\n");
      	printf("2. 查询具体卡\n");
      	printf("0. 退出\n");
      	printf("******************\n");
      }
      
  • 选择卡的类型罗列清单

    • void menu1_3(Standard* pcs) {
      	printf("************************************\n");
      	for (int i = 1; i <= 4; i++) {
      		printf("%d. ", i);
      		change(i);
      		if (pcs[i - 1].state == 1) {
      			printf("==》单位时间内收费%.2lf元\n", pcs[i - 1].price);
      		}
      		else {
      			printf("==》(计费标准)暂未被开发\n");
      		}
      	}
      	printf("0. 退出\n");
      	printf("************************************\n");
      }
      
  • 注销提示菜单

    • void menu1_4() {
      	printf("******************\n");
      	printf("1. 注销卡\n");
      	printf("0. 退出\n");
      	printf("※ 特别注意:注销后无法恢复!\n※ 必须由管理员重新办卡\n※ 管理员请据情况进行余额转移\n");
      	printf("******************\n");
      }
      

1.4 添加卡

addCard(CardManage.c)

  • 导入计费标准~
  • 通过菜单选择卡的类型先~(按0就退出)
  • 非0则进入必须添加一张卡才能退出~
    • newOne–新节点
    • 输入信息~
    • 规则
      • 卡的类型有如下可选(开卡金额不得少于卡的类型对应的单价!)
      • 卡不存在不能建~
      • 记得释放!!!
  • 卡数 + 1
void addCard(Card* pc) {
	Standard* pcs = standardCarry();
	printf("卡的类型有如下可选(开卡金额不得少于卡的类型对应的单价!)\n");
	int input = 0;
	do {
		menu1_3(pcs);
		printf("请输入卡的类型:>");
		scanf("%d", &input);
		if (input != 0) {
			Card* newOne = (Card*)malloc(sizeof(Card));
			printf("请依次输入新卡的卡号,密码,开卡金额\n");
			scanf("%d%s%lf", &newOne->id, newOne->password, &newOne->balance);
			newOne->cardType = input;
			if (pcs[newOne->cardType - 1].state != 1 || newOne->balance < pcs[newOne->cardType].price) {
				printf("添加失败,“可能”是因为开卡金额不足或者此类卡未被开发\n");
                free(newOne);
			}
			else {
				newOne->effect = 1;
				newOne->next = NULL;
				if (cardNumber == 0) {
					memcpy(pc, newOne, sizeof(Card));
				}
				else {
					Card* cur = pc;
					while (cur->next != NULL) {
						cur = cur->next;
					}
					cur->next = newOne;
				}
				cardNumber++;
				printf("添加成功\n");
			}
		}
	} while (input);
	printf("退出成功\n");	
}

1.4.1 对于卡数为0 的情况

  • 使用库里的内存函数memcpy(内存拷贝函数)(string.h)
  • 直接进行内存拷贝转移~
memcpy(pc, newOne, sizeof(Card));

1.5 查询卡

searchCard(CardManage.c)

  • 根据菜单选择操作
    • 打印全部?
    • 打印专门的一个?
    • 退出~
void searchCard(Card* pc) {
	int input = 0;
	do {
		menu1_2();
		scanf("%d", &input);
		switch (input) {
		case 1:
			printAll(pc);
			break;
		case 2:
			printOne(pc);
			break;
		case 0:
			printf("退出成功\n");
			break;
		default:
			printf("输入失败\n");
			break;
		}
	} while (input);
}

1.5.1 打印一个

printOne(CardManage.c)

  • 输入待查询卡号~
  • 遍历链表去找~
  • 找到了按表格打印出来~
    • 各字段根据对应卡号的节点的具体信息进行打印~
    • 这里的表格我借鉴了MySQL数据库的表格~
  1. 占位符的左右对齐==>负号左对齐~
  2. 小数的话,左侧数字为最终小数所占的位数~(包括小数点在内)
void printOne(Card* pc) {
	printf("请输入你要查询的卡号:>");
	int id = 0;
	scanf("%d", &id);
	Card* cur = pc;
	while (cardNumber != 0 && cur != NULL) {
		if (cur->id == id) {
			printf("+------+------+-------------+-------+---------+---------+\n");
			printf("|%-6s|%6s|%13s|%7s|%9s|%9s|\n", "卡号", "密码", "余额", "效应", "卡的种类", "状态");
			printf("+------+------+-------------+-------+---------+---------+\n");
			printf("|%-6d|%6s|%13.2lf|", cur->id, cur->password, cur->balance);
                            //8.2lf代表这小数,总共站位控制在9(不足的时候补齐,足够的时候不用补齐)
			if (cur->effect == 1) {
				printf("%7s|     ", "未注销");
			}
			else {
				printf("%7s|     ", "已注销");
			}
			change(cur->cardType);
			if (cur->state == 1) {
				printf("|%9s|\n", "上机中");
			}
			else {
				printf("|%9s|\n", "未上机");
			}
			printf("+------+------+-------------+-------+---------+---------+\n");
			printf("查找成功\n");
			return;
		}
		cur = cur->next;
	}
	printf("查找失败,“可能”是因为暂无此卡\n");
}
1.5.1.1 通过卡的类型打印对应的名字

change(ChargeStandard.c)

void change(int i) {
	switch (i) {
		case 1:
			printf("年卡");
			break;
		case 2:
			printf("月卡");
			break;
		case 3 :
			printf("日卡");
			break;
		case 4 :
			printf("时卡");
			break;
		default:
			break;
	}
}

1.5.2 打印全部

  • 这个与刚才类似~
  • 直接从头全部遍历打印~

【C语言】网吧管理系统-链表项目设计_第15张图片

void printAll(Card* cur) {
	printf("+------+------+-------------+-------+---------+---------+\n");
	printf("|%-6s|%6s|%13s|%7s|%9s|%9s|\n", "卡号", "密码", "余额", "效应", "卡的种类", "状态");
	printf("+------+------+-------------+-------+---------+---------+\n");
	while (cur != NULL) {
		printf("|%-6d|%6s|%13.2lf|", cur->id, cur->password, cur->balance);
        //8.2lf代表这小数,总共站位控制在9(不足的时候补齐,足够的时候不用补齐)
		if (cur->effect == 1) {
			printf("%7s|     ", "未注销");
		}
		else {
			printf("%7s|     ", "已注销");
		}
		change(cur->cardType);
		if (cur->state == 1) {
			printf("|%9s|\n", "上机中");
		}
		else {
			printf("|%9s|\n", "未上机");
		}
		printf("+------+------+-------------+-------+---------+---------+\n");
		cur = cur->next;
	}
	printf("查找成功\n");
}

1.6 注销卡~

logOffCard(CardManage.c)

  • 根据菜单选择是否继续注销~
void logOffCard(Card* pc) {
	int input = 0;
	do {
		menu1_4();
		scanf("%d", &input);
		if (input) {
			logOff(pc);
		}
		else {
			printf("退出成功\n");
		}
	} while (input);
}

1.6.1 确认注销

logOff(CardManager.c)

  • 找到对应节点~
    • 找到了就直接将effect置为0(effect在一开始被我们自己置为1的)
  • 如果其处在上机状态,则强制下机
    • 这里卖个关子,后续将上下机的时候再详细说说这个操作~
    • 强制下机后费用是按比例计算的,这个也是后续会讲的~
  • 注意:注销之后无法改回来~必须由管理员重新开卡,查询余额据具体操作 ~
void logOff(Card* pc) {
	Card* cur = pc;
	int id = 0;
	char password[7];
	printf("请输入待注销卡的卡号以及对应密码:>");
	scanf("%d%s", &id, password);
	while (cur != NULL && cardNumber != 0) {
		if (id == cur->id && strcmp(password, cur->password) == 0) {
			cur->effect = 0;
			printf("注销成功\n");
			if (cur->state == 1) {
				printf("已强制下机\n");
				Consume* psu = consumeCarry();
				off(cur, psu);
				exitOutConsume(psu);
			}
			return;
		}
		cur = cur->next;
	}
	printf("注销失败,“可能”是因为暂无此卡\n");
}

1.7 退出操作

exitOutCard(Exit.c)

  • 数据导入data(必须自己建立)文件的card.txt二进制文件中

  • 如果卡数为0,那么只讲0导入文件即可,因为这个节点不为NULL但是不是有效数据!

  • 不为0遍历链表导入数据,并且用相同的方法释放空间~

void exitOutCard(Card* pc) {
	FILE* pf = fopen("data\\card.txt", "wb");
	fwrite(&cardNumber, sizeof(int), 1, pf);
	if (cardNumber == 0) {
		return;
	}
	while (pc != NULL) {
		fwrite(pc, sizeof(Card), 1, pf);
		Card* tmp = pc;
		pc = pc->next;
		free(tmp);
	}
	fclose(pf);
}

1.8 测试

  • 代码运行正常

【C语言】网吧管理系统-链表项目设计_第16张图片

  • 二进制文件显示正常~


4. 费用管理~

  • 有了卡这个类之后,进行充值退费操作就比较简单了~
  • 这里是我的计费思想:只有在下机/强制下机结算的钱才是纳入我囊中的钱~
  • 引入新的源文件:ExpenseManage.c
  • 源码在这里:网吧管理系统/ExpenseManage.c · 游离态

【C语言】网吧管理系统-链表项目设计_第17张图片

4.1 主体函数设计

expenseManage(ExpenseManage.c)

  • 导入卡链表~
  • 根据菜单选择操作~
    • 退出的时候更新释放卡链表~
void expenseManage(Manager* pm) {
	int input = 0;
	Card* pc = cardCarry();
	do {
		menu4_1();
		scanf("%d", &input);
		switch (input) {
		case 1:
			recharge(pc);
			break;
		case 2:
			refunt(pc);
			break;
		case 0:
			exitOutCard(pc);
			printf("退出成功\n");
			break;
		default:
			printf("请重新输入\n");
			break;
		}
	} while (input);
}

4.2 充值

recharge(ExpenseManage.c)

  • 输入卡号
    • 找到对应节点
    • 输入充值金额
    • 信息修改~
    • 补充规则:
      • 注销的号是可以充值的,但是不能继续用必须找管理员重新办卡据情况处理
  • 找不到就报无~
void recharge(Card* pc) {
	printf("请输入待充值卡的卡号:>");
	int id = 0;
	scanf("%d", &id);
	while (pc != NULL) {
		if (pc->id == id) {
			printf("请输入充值金额:>");
			double money = 0.0;
			scanf("%lf", &money);
			pc->balance += money;
			printf("充值成功\n");
			printf("\n注意:如果是因为欠费导致的账号注销,注销的卡已作废\n"
				"此次充值若余额恢复非负,管理员请据情况接办理新卡并且余额保留\n");
			return;
		}
		pc = pc->next;
	}
	printf("充值失败,“可能”暂无此卡\n");
}

4.3 退费

refunt(ExpenseManage.c)

  • 输入卡号
    • 找到对应节点~
    • 将一张未注销的卡进行退费~(负数余额必然是注销了的,但是注销了的卡余额可以为正,但无法退费)
      • 余额清空~无法选择退多少
  • 找不到报无~
void refunt(Card* pc) {
	printf("请输入待退费卡的卡号:>");
	int id = 0;
	scanf("%d", &id);
	while (pc != NULL) {
		if (pc->id == id && pc->effect == 1) {//负值时必然注销
			pc->balance = 0.0;
			printf("退费成功\n");
			return;
		}
		pc = pc->next;
	}
	printf("退费失败,“可能”暂无此卡或者此卡已被注销\n");
}

4.4 退出操作

  • 简单的退出~
  • 更新释放卡链表信息~

4.5 测试

【C语言】网吧管理系统-链表项目设计_第18张图片

  • 测试结果正常~

3. 计费管理

  • 接下来是比较复杂的两个环节,计费管理以及查询统计~

  • 计费管理操作上机下机,下机的时候计费~

    • 需求:
      • 记录上机记录
      • 记录下机消费记录
    • 这些记录就是为了查询统计而生的~
  • 引入新的源文件:ChargeManage.c

  • 源码在这:网吧管理系统/ChargeManage.c · 游离态

【C语言】网吧管理系统-链表项目设计_第19张图片

3.1 两个记录性结构体~

3.1.1 Consume 消费记录(basis.h)

typedef struct Consume {
	int id;//卡号
	long long  timeNode;//时间节点
	double money;//下机则为实际收费,合理是相同的
	struct Consume* next;//后继
}Consume;
  1. 卡号
  2. 对应交易时间时间戳
  3. 交易金额(按比例/按单位)
  4. 后继节点~

3.1.2 Puncher 上机记录(basis.h)

//上机记录
typedef struct Puncher {
	int id;//卡号
	long long  timeNode;//时间节点
	struct Puncher* next;//后继
}Puncher;
  1. 卡号
  2. 对应上机时间时间戳
  3. 后继节点~

3.2 主体函数设计~

chargeManage(ChargeManage.c)

  • 引入新的全局变量

    • orderNumber消费单数~
    • workNumber上机记录数据数~
  • 导入卡号,消费记录,上机记录~

  • 根据菜单进行选择~

    • 上机
    • 下机
    • 退出则更新释放三条链表~
void chargeManage(Manager* pm) {
	int input = 0;
	Card* pc = cardCarry();
	Consume* psu = consumeCarry();
	Puncher* ppu = puncherCarry();
	do {
		menu3_1();
		scanf("%d", &input);
		switch (input) {
		case 1 :
			onComputer(pc, ppu);
			break;
		case 2 :
			offComputer(pc, psu);
			break;
		default:
			printf("输入错误\n");
			break;
		case 0:
			exitOutCard(pc);
			exitOutConsume(psu);
			exitOutPuncher(ppu);
			printf("退出成功\n");
			break;
		}
	} while (input);
}

3.2.1 读取文件操作

3.2.1.1 consumeCarry(Carry.c)
  • 读取data文件中的consume.txt二进制文件
Consume* consumeCarry() {
	FILE* pf = fopen("data\\consume.txt", "rb");
	Consume* psu = (Consume*)malloc(sizeof(Consume));//堆区空间,不会被收回
	if (pf == NULL) {
		pf = fopen("data\\consume.txt", "wb");
		fwrite(&orderNumber, sizeof(int), 1, pf);
		fclose(pf);
	}
	else {
		fread(&orderNumber, sizeof(int), 1, pf);
		fread(psu, sizeof(Consume), 1, pf);
		Consume* cur = psu;
		for (int i = 1; i < orderNumber; i++) {
			Consume* newOne = (Consume*)malloc(sizeof(Consume));
			fread(newOne, sizeof(Consume), 1, pf);
			newOne->next = NULL;
			cur->next = newOne;
			cur = cur->next;
		}
	}
	return psu;
}
  1. 文件指针为NULL代表打不开文件,说明文件不存在
    • 则我只需要建立一个,并且将整数0导入文件中~
    • 我的一个习惯:我会讲元素个数放在文件的首位
  2. 文件打开了,进行标准的读取操作
    • 首先拿到消费单数orderNumber~
    • 然后获得第一单~
    • 后续以尾插的形式进行插入~
      • 后续应该判断第一个节点是否是有效数据~
  3. 重点:要将尾节点的后驱置为NULL,否则会导致野指针异常/死循环~
3.2.1.2 puncherCarry(Carry.c)
  • 读取data文件中的puncher.txt二进制文件
Puncher* puncherCarry() {
	FILE* pf = fopen("data\\puncher.txt", "rb");
	Puncher* ppu = (Puncher*)malloc(sizeof(Puncher));//堆区空间,不会被收回
	if (pf == NULL) {
		pf = fopen("data\\puncher.txt", "wb");
		fwrite(&workNumber, sizeof(int), 1, pf);
		fclose(pf);
	}
	else {
		fread(&workNumber, sizeof(int), 1, pf);
		fread(ppu, sizeof(Puncher), 1, pf);
		Puncher* cur = ppu;
		for (int i = 1; i < workNumber; i++) {
			Puncher* newOne = (Puncher*)malloc(sizeof(Puncher));
			fread(newOne, sizeof(Puncher), 1, pf);
			newOne->next = NULL;
			cur->next = newOne;
			cur = cur->next;
		}
	}
	return ppu;
}
  • 与刚才是一样的~
  1. 文件指针为NULL代表打不开文件,说明文件不存在
    • 则我只需要建立一个,并且将整数0导入文件中~
    • 我的一个习惯:我会讲元素个数放在文件的首位
  2. 文件打开了,进行标准的读取操作
    • 首先拿到上机记录数据数workNumber~
    • 然后获得第一份数据~
    • 后续以尾插的形式进行插入~
      • 后续应该判断第一个节点是否是有效数据~
  3. 重点:要将尾节点的后驱置为NULL,否则会导致野指针异常/死循环~

3.3 菜单

  • 上下机操作菜单

    • 提醒顾客过早下机会导致浪费~

    • void menu3_1() {
      	printf("****************************************\n");
      	printf("※ 请提示顾客:时间较长的卡请勿过早下机\n"
      		"电脑时刻开着呢,不需要再次上机\n");
      	printf("0. 退出\n");
      	printf("1. 上机\n");
      	printf("2. 下机\n");
      	printf("****************************************\n");
      }
      

3.4 上机

conComputer(ChargeMange.c)

  • 输入卡号密码确认是否符合身份
    • 上机中/已注销无法再上机
    • 将上机记录信息记录在ppu中~
    • 上机成功后用时间函数报当时时间信息
  • 找不到即报无~
void onComputer(Card* pc, Puncher* ppu) {
	printf("请输入卡号与密码:>");
	Card* cur = pc;
	int id = 0;
	char password[7];
	scanf("%d%s", &id, password);
	while (cardNumber != 0 && cur != NULL) {
		if (cur->id == id && strcmp(password, cur->password) == 0) {
			if (cur->effect != 1) {
				printf("此卡已被注销,无法上机\n");
				return;
			}
			else {
				if (cur->state == 1) {
					printf("已经是上机状态,本次操作失效\n");
				}
				cur->state = 1;
				time_t t = time(NULL);
				cur->upTime = t;
				//上机记录~~
				clockIn(cur, ppu);
				//上机记录~~
				printf("--------------------------\n");
				printf("上机成功\n");
				printf("时间:%s", ctime(&t));
				printf("--------------------------\n");
			}
			return;
		}
		cur = cur->next;
	}
	printf("上机失败,“可能”原因是此卡暂为开通\n");
}

3.4.1 上机记录函数clockIn(ChargeManage.c)

void clockIn(Card* cur, Puncher* ppu) {
	Puncher* newOne = (Puncher*)malloc(sizeof(Puncher));
	newOne->id = cur->id;
	newOne->next = NULL;
	newOne->timeNode = cur->upTime;
	if (workNumber == 0) {
		memcpy(ppu, newOne, sizeof(Puncher));
	}
	else {
		Puncher* current = ppu;
		while (current->next != NULL) {
			current = current->next;
		}
		current->next = newOne;
	}
	workNumber++;
}
  • 重点要处理workNumber为0时的那个假数据!
    • 用memcpy(库里内存函数,string.h)
  • 新节点newOne记录信息后,尾插到链表尾
  • workNumber上机记录数据数 + 1

3.5 下机

offnComputer(ChargeMange.c)

  • 输入卡号密码确认是否符合身份
    • 未上机/卡的计费标准不存在的情况下,无法下机
    • 将消费记录信息记录在psu中~
      • 对于余额此刻小于0后,强制注销~
    • 下机成功后用时间函数报当时时间信息
  • 找不到即报无~
void offComputer(Card* pc, Consume* psu) {
	printf("请输入卡号与密码:>");
	Card* cur = pc;
	int id = 0;
	char password[7];
	scanf("%d%s", &id, password);
	Standard* pcs = standardCarry();
	while (cardNumber != 0 && cur != NULL) {
		if (cur->id == id && strcmp(password, cur->password) == 0) {
			if (cur->state == 0) {
				printf("已经是下机状态,本次操作失效\n");
				return;
			}
			if (pcs[cur->cardType - 1].state == 1) {//未上机
				time_t t = time(NULL);
				long long longTime = t - cur->upTime;
				double gap = 1.0 * longTime / transfer(cur->cardType);
				if (gap != (int)gap) {
					gap = (int)gap + 1;
				}
				//记录收益记录~~
				settlement(cur, psu, t, gap, pcs);
				//记录收益记录~~
				cur->state = 0;
				if (cur->balance < 0) {
					printf("此卡已欠费,账号已被注销,请充值缴费\n");
					cur->effect = 0;
				}
				printf("--------------------------\n");
				printf("下机成功, 账户已更新\n");//下机则结算
				printf("时间:%s", ctime(&t));
				printf("--------------------------\n");
			}
			else {
				printf("下机失败~请补充计费标准~\n");
			}
			exitOutStandard(pcs);//释放空间
			return;
		}
		cur = cur->next;
	}
	printf("下机失败,“可能”原因是此卡暂为开通\n");
}

3.5.1 transfer(计费标准转化秒数函数 ChargeManage.c)

//3600------1h
//86400-----1day
//2592000---1month//默认30天
//31536000--1year//默认365天
long long transfer(int i) {
	switch (i) {
		case 1 :	
			return 31536000;
		case 2 :	
			return 2592000;
		case 3 :	
			return 86400;
		case 4 :	
			return 3600;
	}
}

3.5.2 局部变量:gap

  • 例如年卡,一年以内gap为1,一年到两年之间gap为2,恰好为两年gap为2~
  • 表示几个单价~
		time_t t = time(NULL);
		long long longTime = t - cur->upTime;
		double gap = 1.0 * longTime / transfer(cur->cardType);
		if (gap != (int)gap) {
			gap = (int)gap + 1;//不足进1~
		}

3.5.3 下机消费记录函数settlement(ChargeManage.c)

void settlement(Card* cur, Consume* psu, time_t t, double gap, Standard* pcs) {
	
    double benifit = gap * pcs[cur->cardType - 1].price;
	cur->balance -= benifit;
	Consume* newOne = (Consume*)malloc(sizeof(Consume));
	newOne->id = cur->id;
	newOne->money = benifit;
	newOne->next = NULL;
	newOne->timeNode = t;

	if (orderNumber == 0) {
		memcpy(psu, newOne, sizeof(Consume));
	}
	else {
		Consume* current = psu;
		while (current->next != NULL) {
			current = current->next;
		}
		current->next = newOne;
	}
	orderNumber++;
}
  • 根据gap和计费标准pcs确定交易金~
  • 制作newOne新节点
  • 将新节点插入到psu的链表尾
    • 处理orderNumber为0时头结点为假数据的情况~
  • orderNumber 消费单数 + 1

3.6 退出操作

  • 对三链表进行释放更新处理~
  • 卡链表的退出已在上文书写

3.6.1 消费单表的更新

exitOutConsume(Exit.c)

  • 数据导入data(必须自己建立)文件的consume.txt二进制文件中

  • 如果卡数为0,那么只讲0导入文件即可,因为这个节点不为NULL但是不是有效数据!

  • 不为0遍历链表导入数据,并且用相同的方法释放空间~

void exitOutConsume(Consume* psu) {
	FILE* pf = fopen("data\\consume.txt", "wb");
	fwrite(&orderNumber, sizeof(int), 1, pf);
	if (orderNumber == 0) {
		return;
	}
	while (psu != NULL) {
		fwrite(psu, sizeof(Consume), 1, pf);
		Consume* tmp = psu;
		psu = psu->next;
		free(tmp);
	}
	fclose(pf);
}

3.6.2 上机记录数据表

exitOutPunche(Exit.c)

  • 数据导入data(必须自己建立)文件的puncher.txt二进制文件中

  • 如果卡数为0,那么只讲0导入文件即可,因为这个节点不为NULL但是不是有效数据!

  • 不为0遍历链表导入数据,并且用相同的方法释放空间~

void exitOutPuncher(Puncher* ppu) {
	FILE* pf = fopen("data\\puncher.txt", "wb");
	fwrite(&workNumber, sizeof(int), 1, pf);
	if (workNumber == 0) {
		return;
	}
	while (ppu != NULL) {
		fwrite(ppu, sizeof(Puncher), 1, pf);
		Puncher* tmp = ppu;
		ppu = ppu->next;
		free(tmp);
	}
	fclose(pf);
}

3.7 补充:注销导致的强制下机计费

  • 前面下机导致的注销,信息已记录
  • 但是注销导致的下机,尚未解决(下面操作出现在卡管理的注销操作中~
    • 【C语言】网吧管理系统-链表项目设计_第20张图片
  1. 导入消费单表

  2. 记录强制下机消费记录

  3. 释放更新消费单consume.txt~

3.7.1 强制下机消费记录函数 off (CardManage.c)

void off(Card* pc, Consume* psu) {
	printf("由于此次下机非主动下机,所以此次消费以按比例计算\n");
	Standard* pcs = standardCarry();
	time_t t = time(NULL);
	long long longTime = t - pc->upTime;
	double gap = 1.0 * longTime / transfer(pc->cardType);
	settlement(pc, psu, t, gap, pcs);
	pc->state = 0;
}
  • 为什么gap的类型我要定位double,原因就是我还要再次使用settlement函数
  • 这里我规则是:注销导致强制下机,交易金是以时间占比计算的~
    • 以此计算gap~
  • 调用settlement下机消费记录函数~
  • 并将卡的上机状态改为0 =>下机~

3.8 测试

  • 卡的情况

【C语言】网吧管理系统-链表项目设计_第21张图片

  • 测试结果正常~
    • 至于消费记录以及上机记录,再查询统计的时候一起测试~

【C语言】网吧管理系统-链表项目设计_第22张图片

  • 二进制文件显示正常~


5. 查询统计

  • 来到最后一个环节啦啦啦~ ==> 查询统计
      1. 查询单一卡一段时间的消费
      2. 查询全部卡一段时间额总消费
      3. 查询近一年来每个月的营销额以及上机次数~
  • 引出一个新的源文件:SearchStatistics.c
  • 源码在这:网吧管理系统/SearchStatistics.c · 游离态
    【C语言】网吧管理系统-链表项目设计_第23张图片

5.1 主体函数设计

searchStatistics(SearchStatistics.c)

  • 导入卡链表
  • 根据菜单选择对应操作~
void searchStatistics(Manager* pm) {
	int input = 0;
	Card* pc = cardCarry();
	void (*func[4])(Card * pc) = 
	{ exitOutStatistics, searchConsume, statisticsTime, statisticsMonths };
	do {
		menu5_1();
		scanf("%d", &input);
		if (input <= 3 && input >= 0) {
			func[input](pc);
		}
		else {
			printf("输入失败\n");
		}

	} while (input);
}

5.1.1 函数指针数组

  • 这里由于选项较多,我选择用函数指针数组,用法与刚才一样,要注意下标与菜单与对应选项要相符合哦~

5.2 菜单

  • 统计菜单
void menu5_1() {
	printf("******************\n");
	printf("0. 退出\n");
	printf("1. 查询消费记录\n");
	printf("2. 统计总营业额\n");
	printf("3. 统计月营业额\n");
	printf("******************\n");
}
  • 选择时间段菜单
void menu5_2() {
	printf("******************\n");
	printf("1. 最近一年\n");
	printf("2. 最近一月\n");
	printf("3. 最近一天\n");
	printf("******************\n");
}

5.3 查询单一卡一段时间的消费~

searchConsume(SearchStatistics.c)

  • 输入卡号,探路指针找到对应卡~

    • 找到后通过菜单选择一个时间段
    • 调用consumeCarry去导入消费记录链表
    • 通过消费记录链表节点的时间属性去判断是否属于该时间段
      • 若属于且是对应卡,则以表格打印出来~
        • 该行用箭头指向右侧的结算时间~
      • 打印思路:(打印的同时用中间寄存器释放空间)
        • 如何对齐请看上文~
        • 【C语言】网吧管理系统-链表项目设计_第24张图片
  • 找不到报无~

void searchConsume(Card* pc) {
    //三个选择,最近一天,最近一月,最近一年
	printf("请输入一张卡的卡号:>");
	int id = 0;
	scanf("%d", &id);
	Card* cur1 = pc;
	while (cardNumber != 0 && cur1 != NULL) {
		if (id == cur1->id) {
			menu5_2();
			int number = 0;
			printf("请选择一个时间段(距现在):>");
			scanf("%d", &number);
			long long gapTime = transfer(number);
			Consume* psu = consumeCarry();
			Consume* cur2 = psu;
			printf("+------+-------------+\n");
			printf("|%-6s|%13s|\n", "卡号", "消费/元");
			printf("+------+-------------+\n");
			while (cur2 != NULL) {
				Consume* tmp = cur2;
				if (cur2->id == id && time(NULL) - cur2->timeNode <= gapTime) {
					printf("|%-6d|%13.2lf| 结算时间===>%s", cur2->id, cur2->money, ctime(&cur2->timeNode));
					printf("+------+-------------+\n");
				}
				cur2 = cur2->next;
				free(tmp);
			}
			return;
		}
		cur1 = cur1->next;
	}
	printf("查询失败,暂无此卡\n");
}

5.3.1 细讲判断时间段方法~

【C语言】网吧管理系统-链表项目设计_第25张图片

  • 调用transfer,将选择的数字转化为对应时间戳(一个单位时间
  • 如果满足要求且是对应的那张卡,打印下来~

5.4 查询所有卡一段时间的总消费

statisticsTime(SearchStatistics.c)

  • 通过菜单选择一个时间段
  • 调用consumeCarry去导入消费记录链表
  • 引入新的局部变量sum,计算总营业额~
  • 通过消费记录链表节点的时间属性去判断是否属于该时间段
    • 若属于,则以表格打印出来~
      • 该行用箭头指向右侧的结算时间~
    • 打印思路:(打印的同时用中间寄存器释放空间)
      • 如何对齐请看上文~

      • 【C语言】网吧管理系统-链表项目设计_第26张图片

      • 并在最后,打印该时间段的总营业额~

void statisticsTime(Card* pc) {
	menu5_2();
	int number = 0;
	printf("请选择一个时间段统计总营业额(距现在):>");
	scanf("%d", &number);
	Consume* psu = consumeCarry();
	Consume* cur = psu;
	printf("+------+-------------+\n");
	printf("|%-6s|%13s|\n", "卡号", "消费/元");
	printf("+------+-------------+\n");
	double sum = 0;
	long long gapTime = transfer(number);
	while (cur != NULL) {
		Consume* tmp = cur;
		if (time(NULL) - cur->timeNode <= gapTime) {
			printf("|%-6d|%13.2lf| 结算时间===>%s", cur->id, cur->money, ctime(&cur->timeNode));
			printf("+------+-------------+\n");
			sum += cur->money;
		}
		cur = cur->next;
		free(tmp);
	}
	printf("|%-6s|%13.2lf|\n", "共计", sum);
	printf("+------+-------------+\n");
}

【C语言】网吧管理系统-链表项目设计_第27张图片

5.5 查询统计近一年来每个月的营销额以及上机次数~

statisticsMonths(SearchStatistics.c)

  • 别看代码多,其实模块化很明确~
    • 后面细腻分析~
  • 大致思路是
    1. 通过计算获取现在是第几年第几个月
    2. 然后推导出近12个月
    3. 通过十二个月每一个特定的时间戳范围查询统计划分~
    4. 导入消费记录和上机数据记录~
    5. 遍历多次出打印结果~
void statisticsMonths(Card* pc) {
	Date date = judgeMonth(time(NULL));
	int year = date.year;
	int month = date.month;
	Date dates[12];
	dates[11] = date;
	for (int i = 10; i >= 0; i--) {
		month--;
		if (month == 0) {
			year--;
			month = 12;
		}
		dates[i].timestamp = dates[i + 1].timestamp - months[judgeLeapYear(year)][month] * 86400;
		dates[i].month = month;
		dates[i].year = year;
	}
	FILE* pf = fopen("data\\statisticsMonths.txt", "w");
	Consume* psu = consumeCarry();
	Puncher* ppu = puncherCarry();
	Consume* cur = psu;
	Puncher* current = ppu;
	//做成哈希,还有这样做,构建的过程都要O(N^2)

	//这里是下机计费,所以很有可能上机次数多但是收益少
	fprintf(pf, "+-------+---------------+---------------+\n");
	fprintf(pf, "|%-7s|%15s|%15s|\n", "Month", "Hands-on times", "Total turnover");
	fprintf(pf, "+-------+---------------+---------------+\n");
	printf("+-------+--------+-------------+\n");
	printf("|%-7s|%8s|%13s|\n", "年月份", "上机次数", "月总营销额/元");
	printf("+-------+--------+-------------+\n");


	for (int i = 0; i < 12; i++) {
		cur = psu;
		current = ppu;
		Date d = dates[i];
		long long min = d.timestamp;
		long long max = min + months[judgeLeapYear(d.year)][d.month] * 86400;
		double sum = 0.0;
		int count = 0; //上机次数

		//消费与上机次数统计
		while (orderNumber != 0 && cur != NULL &&
			workNumber != 0 && current != NULL) {
			if (cur->timeNode < max && cur->timeNode >= min) {
				sum += cur->money;
			}
			if (current->timeNode < max && current->timeNode >= min) {
				count++;
			}
			cur = cur->next;
			current = current->next;
		}
		while (orderNumber != 0 && cur != NULL) {
			if (cur->timeNode < max && cur->timeNode >= min) {
				sum += cur->money;
			}
			cur = cur->next;
		}
		while (workNumber != 0 && current != NULL) {
			if (current->timeNode < max && current->timeNode >= min) {
				count++;
			}
			current = current->next;
		}


		fprintf(pf, "|%4d.%2d|%15d|%15.2lf|\n", d.year, d.month, count, sum);
		fprintf(pf, "+-------+---------------+---------------+\n");
		printf("|%4d.%2d|%8d|%13.2lf|\n", d.year, d.month, count, sum);
		printf("+-------+--------+-------------+\n");

	}
	fclose(pf);
	//free掉
	exitOutConsume(psu);
	exitOutPuncher(ppu);
}

5.5.1 引入 Date 年月结构体(basis.h)

//日期年月
typedef struct {
	int year;
	int month;
	long long timestamp;//首时间戳
}Date;
  1. 此年此月的00:00:00时的时间戳~

5.5.2 引入全局变量:润平年月份日数对应数组~

months(SearchStatistics.c)

int months[2][13] = { { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 },
		{0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31} };
  1. 平年 – 2月 28天~
  2. 闰年 – 2月 29天~

5.5.3 判断闰年函数以及判断年月函数~

  1. judgeLeapYear(Searchstatistics.c)

  2. judgeMonth(SearchStatistics.c)

int judgeLeapYear(int year) {
	return (year % 400 == 0) || (year % 4 == 0 && year % 100 != 0);
}
Date judgeMonth(long long timeStamp) {
	long long times = 1672502400;
	int year = 2023;
	int month = 1;
	int i = 0;
	while ((times += months[judgeLeapYear(year)][month] * 86400) <= timeStamp) {
		i++;
		month = i % 12 + 1;
		if (month == 1) {
			year++;
		}
	}
	long long timestamp = times - months[judgeLeapYear(year)][month] * 86400;
	Date date = { year, month, timestamp};
	return date;
}
  • 我这里是从2023年开始算起,因为现在不可能是2023年之前~

    • 通过计算,2023-1-1 00:00:00 的时候的时间戳为1672502400~
  • 这里用到返回结构体变量的技巧去返回两个值~
    【C语言】网吧管理系统-链表项目设计_第28张图片

5.5.4 推导近12个月

  • 建立一个12大的dates数组
  • 最后一个元素便是judgeMonth(time(NULL))的返回值~
    • 记录year,month
    • 逆推出近12个月~
      • 结合年份月份天数数组,计算对应月份的“首时间戳”~
      • dates[i].timestamp = dates[i + 1].timestamp - months[judgeLeapYear(year)][month] * 86400;
    • 但month为0时,说明是翻到了前年~
      • 应做出处理~
	Date date = judgeMonth(time(NULL));
	int year = date.year;
	int month = date.month;
	Date dates[12];
	dates[11] = date;
	for (int i = 10; i >= 0; i--) {
		month--;
		if (month == 0) {
			year--;
			month = 12;
		}
		dates[i].timestamp = dates[i + 1].timestamp - months[judgeLeapYear(year)][month] * 86400;
		dates[i].month = month;
		dates[i].year = year;
	}

5.5.5 建立文本文档记录 + 打印到屏幕~

  • 引入新文件 "data\\statisticsMonths.txt",建立文本文档~,用“w”
    • 学校要求~
  • 引入两个探路指针~
  • 列表头写入文件中并且打印在屏幕上~
  • 显示的跟记录的不一样的原因是:
    • C语言导出的文本文档中文格式不好看,不同文本编辑器打开都有可能不一样,可能乱码,得把文本编辑器编码方式改为ANSI~
    • 并且很难对齐~
	FILE* pf = fopen("data\\statisticsMonths.txt", "w");
	Consume* psu = consumeCarry();
	Puncher* ppu = puncherCarry();
	Consume* cur = psu;
	Puncher* current = ppu;
	//做成哈希,还有这样做,构建的过程都要O(N^2)

	//这里是下机计费,所以很有可能上机次数多但是收益少
	fprintf(pf, "+-------+---------------+---------------+\n");
	fprintf(pf, "|%-7s|%15s|%15s|\n", "Month", "Hands-on times", "Total turnover");
	fprintf(pf, "+-------+---------------+---------------+\n");
	printf("+-------+--------+-------------+\n");
	printf("|%-7s|%8s|%13s|\n", "年月份", "上机次数", "月总营销额/元");
	printf("+-------+--------+-------------+\n");
  • 后续打印方式还是这样:
    • 【C语言】网吧管理系统-链表项目设计_第29张图片

5.5.6 遍历12次打印表格~

for (int i = 0; i < 12; i++) {
	cur = psu; //回到一开始
	current = ppu; //回到一开始
	Date d = dates[i];
	long long min = d.timestamp;
	long long max = min + months[judgeLeapYear(d.year)][d.month] * 86400;
	double sum = 0.0;
	int count = 0; //上机次数

	//消费与上机次数统计
	while (orderNumber != 0 && cur != NULL &&
		workNumber != 0 && current != NULL) {
		if (cur->timeNode < max && cur->timeNode >= min) {
			sum += cur->money;
		}
		if (current->timeNode < max && current->timeNode >= min) {
			count++;
		}
		cur = cur->next;
		current = current->next;
	}
    
	while (orderNumber != 0 && cur != NULL) {
		if (cur->timeNode < max && cur->timeNode >= min) {
			sum += cur->money;
		}
		cur = cur->next;
	}

	while (workNumber != 0 && current != NULL) {
		if (current->timeNode < max && current->timeNode >= min) {
			count++;
		}
		current = current->next;
	}
	fprintf(pf, "|%4d.%2d|%15d|%15.2lf|\n", d.year, d.month, count, sum);
	fprintf(pf, "+-------+---------------+---------------+\n");
	printf("|%4d.%2d|%8d|%13.2lf|\n", d.year, d.month, count, sum);
	printf("+-------+--------+-------------+\n");

}
fclose(pf);
//free掉
exitOutConsume(psu);
exitOutPuncher(ppu);
  • 引入两个局部变量,规定时间区间
    • min为此年月对应的首时间戳
    • max为下个月对应的首时间戳
    • 时间区间为 [min, max) (左闭右开)
  • 引入两个局部变量,记录
    • sum记录月营销额~
    • count记录月上机次数~
  • 开始遍历,一开始两个链表一起遍历,当然是有一个先停下来的可能的
    • 停下来后另一个继续跑
  • 然后,将数据按对应格式写入到文件中 并且打印在屏幕上~

总共遍历个12次

  • 最后,关闭文件(如果不关闭,那么数据就只会在程序的终结才能从缓存区导入文件!)

    • //释放空间
      exitOutConsume(psu);
      exitOutPuncher(ppu);
      

5.6 退出操作

  • 超级简单地退出~
void exitOutStatistics(Card* pc) {
	printf("退出成功\n");
}

5.7 测试~

  • 程序运行正常~

【C语言】网吧管理系统-链表项目设计_第30张图片

【C语言】网吧管理系统-链表项目设计_第31张图片


文章到此结束!谢谢观看
可以叫我 小马,我可能写的不好或者有错误,但是一起加油鸭

这是我的代码仓库!(在马拉圈的23.3里)马拉圈2023年三月: 大学生代码仓库

全部源码具体位置:网吧管理系统 . 游离态

邮箱[email protected]

你可能感兴趣的:(链表,c语言,数据结构,开发语言)