这篇文章的一些修正已经更新:修正版
C语言 & 网吧管理系统
- 本文章是我(东莞理工学院)对于学校期末程序设计项目的总结,可以借鉴,请勿抄袭~
- 数据结构:顺序表,链表
- 知识点:C基础语法,C进阶语法,C文件操作
- 内容简单易懂,难点主要在于项目代码思想思路设计,此系统可能有所缺陷,希望大家能提供建议!我特别需要~
- 后附上源码,望大家支持~
- 本文章可以很好的训练C语言语法,也很好的提高工程思维~
总起. 功能以及代码排版要求
重要前提: 你在下面学习的过程将可能会比较困惑,但是很大可能结合其后面所讲的内容,可以解决你的问题! 耐心耐心耐心
- 多数存储方式应使用链式存储!
- 这是学校的要求,不过我认为有些可以用到不同的数据结构来存储,例如适合查询的哈希表~
- 每个功能都有单独的函数—这也是很重要的习惯~
- 函数命名我采用小驼峰【就是单词与单词之间,用大写字母分割】
- 对于每个模块,放在不用的源文件,好做区分,而申明均放在一个头文件中
- 只需要包含这个头文件,就可以用里面的东西啦~
- 函数调用的话,会跳转到特定的源文件,所以有一些函数不需要申明,加上static也不错~
- 这里static有点像Java的private~
- 我这个思想习惯于Java的类
- 包括对应的链表的节点数,我是用一个全局变量代替
- 不能用static修饰(包括函数也不行),因为申明了也不能给别的源文件使用~
- 即使不申明,也不可以重名~
- 不像Java面向对象,C语言这里还是很难像Java那样方便
- 包括对应的链表的节点数,我是用一个全局变量代替
序号 | 模块 | 功能 | 说明 |
---|---|---|---|
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 · 游离态
- 后续讲得比较分散,可能会导致哪些东西不知道要放哪,所以你可以尝试看一看全部的源码~
7. 系统
开启系统:
- 登录
- 进入系统后登录~
- 确定身份后登录后进行其他操作,才有登出操作
- 登出后可以选择继续登录
- 关闭系统
- 不选择继续登录,可选择是否再次开启系统~
退出选择:
- 程序的终结~
- 真正意义上的完全退出~
- 真正意义上的完全退出~
7.1 主函数设计~
-
建立一个源文件
test.c
,头文件basis.h
-
此时在头文件已有信息:
-
重点说明:不同源文件的信息只需要在头文件申明就好,因为每个源文件都包含了这个basis.h头文件~
- 并且会在后方填入写在哪个文件,并添加函数是在调用者函数的上方~
-
#pragma once//防止重复包含头文件 //不要在这个文件开始运行程序~~~会报错! //并且上方列表如无出现test.c,也会报错 #define _CRT_SECURE_NO_WARNINGS 1 //系统头文件 #include<stdio.h> #include<stdlib.h> //动态内存分配函数~ 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 是没有价值的,下次读取这个地址也没用~
-
-
-
对于读取成功,进行内容导入~
-
改变全局变量
managerNumber
fread(&managerNumber, sizeof(int), 1, pf);
-
此时文件不可能没有内容,因为经过初始化了
Manager* pm = (Manager*)malloc(sizeof(Manager)); fread(pm, sizeof(Manager), 1, pf); Manager* cur = pm; head = pm;//用全局head -> 记录头结点地址~
-
读取文件并关闭文件~
- 一个元素一个元素的读取,以尾插法延伸~
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<string.h>
(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<time.h>
(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; }
- 将管理员个数导入文件
- 从头开始遍历,将每一个节点导入文件
- 同时将每一部分空间进行释放~
- 不释放,空间泄露可能会很危险,对于一些大工程而言
- 所以要有这个习惯~
7.4.2 退出操作文件 - Exit.c
- 我将所有退出操作放进一个源文件中
- 方便管理~
- 源码在这里:网吧管理系统/Exit.c · 游离态
7.5 测试
- 代码运行正常~
- 二进制文件显示正常~
- 因为VS2022中,在内存中,数据存储是小端存储~
- 小端存储就是,真值小的字节地址小,真值大的字节地址大
- 大端就是反着来
- 所以下图的06 00 00 00,8位十六进制数字,在内存中对应的int整形就是6~
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"); }
-
无法删除超级管理员~
-
输入编号姓名删除管理员
-
遍历链表查找(由于编号唯一,理当只删一个,不应该在添加管理员时使用相同编号,但是我这个项目不会查重
-
找到了删完return
- 这里的思路是:(删除节点操作)
- pm是本人,绝对不是被删除的,所以只需要判断后面的就行
- 每次都判断后驱的那个是不是要被删的
- 是的话,记录待被删除的节点(要free哦)
Manager* rubbish = cur->next;
cur->next = rubbish->next;
越过节点,完成删除~free(rubbish);
managerNumber--;
管理员数量减一
- 这里的思路是:(删除节点操作)
-
找不到打印找不到的信息~
-
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去跑到末尾~
- 判断id是否合理(不合理直接退出)
- 合理即添加
6.4.2 设置权限
-
此时的cur指向的就是新增节点~
-
通过多次输入选项,对对应的权限进行设置
-
按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 测试
- 测试结果正常~
2. 计费标准~
- 也很好理解,有了计费标准,才能有一张卡嘛~
- 我的思路就是:分为年月日时四种卡,这样就固定了这个计费标准就一直都是四个
- 时间较长的卡不应该太早下机~即使当天不玩,也可以不下机
新源文件ChargeStandard.c 源码在这里:网吧管理系统/ChargeStandard.c · 游离态
2.1 计费标准结构体
Standard
(basis.h)
//计费标准
typedef struct Standard {
int type;//1 2 3 4
int state;//决定此卡是否能办
double price;//标准单价
}Standard;
- type => 卡的类型~
- state=> 是否有此标准~
- 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;
}
- 标准的个数一直都是4个,顺序一定1 2 3 4,只不过state可能有所不同,所以我用的是顺序表
- 打开文件失败则必然是不存在此文件,则应该进行判断~
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 测试
-
下面的测试案例,都是事先被我插入数据的~
-
运行正常~
-
二进制文件显示正常~
1. 卡管理~
- 有了计费标准之后,我们基本可以开始构建卡了~
- 引入新的源文件:CardManage.c
- 源码在这里:网吧管理系统/CardManage.c · 游离态
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;
-
卡号
-
密码
-
余额
-
效应—注销与否
-
卡的类型—计费标准
-
上机的时间的时间戳~(64位长整形)
-
状态—上机了与否
-
后继节点—构造链表
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;
}
- 文件指针为NULL代表打不开文件,说明文件不存在
- 则我只需要建立一个,并且将整数0导入文件中~
- 我的一个习惯:我会讲元素个数放在文件的首位~
- 刚才的计费标准没有是因为一直都是4个~
- 文件打开了,进行标准的读取操作
- 首先拿到卡数~
- 然后获得第一张卡~
- 后续以尾插的形式进行插入~
- 重点:要将尾节点的后驱置为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数据库的表格~
- 占位符的左右对齐==>负号左对齐~
- 小数的话,左侧数字为最终小数所占的位数~(包括小数点在内)
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 打印全部
- 这个与刚才类似~
- 直接从头全部遍历打印~
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 测试
- 代码运行正常
- 二进制文件显示正常~
4. 费用管理~
- 有了卡这个类之后,进行充值退费操作就比较简单了~
- 这里是我的计费思想:只有在下机/强制下机结算的钱才是纳入我囊中的钱~
- 引入新的源文件:ExpenseManage.c
- 源码在这里:网吧管理系统/ExpenseManage.c · 游离态
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 测试
- 测试结果正常~
3. 计费管理
-
接下来是比较复杂的两个环节,计费管理以及查询统计~
-
计费管理操作上机下机,下机的时候计费~
- 需求:
- 记录上机记录
- 记录下机消费记录
- 这些记录就是为了查询统计而生的~
- 需求:
-
引入新的源文件:ChargeManage.c
3.1 两个记录性结构体~
3.1.1 Consume 消费记录(basis.h)
typedef struct Consume {
int id;//卡号
long long timeNode;//时间节点
double money;//下机则为实际收费,合理是相同的
struct Consume* next;//后继
}Consume;
- 卡号
- 对应交易时间时间戳
- 交易金额(按比例/按单位)
- 后继节点~
3.1.2 Puncher 上机记录(basis.h)
//上机记录
typedef struct Puncher {
int id;//卡号
long long timeNode;//时间节点
struct Puncher* next;//后继
}Puncher;
- 卡号
- 对应上机时间时间戳
- 后继节点~
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;
}
- 文件指针为NULL代表打不开文件,说明文件不存在
- 则我只需要建立一个,并且将整数0导入文件中~
- 我的一个习惯:我会讲元素个数放在文件的首位
- 文件打开了,进行标准的读取操作
- 首先拿到消费单数orderNumber~
- 然后获得第一单~
- 后续以尾插的形式进行插入~
- 后续应该判断第一个节点是否是有效数据~
- 重点:要将尾节点的后驱置为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;
}
- 与刚才是一样的~
- 文件指针为NULL代表打不开文件,说明文件不存在
- 则我只需要建立一个,并且将整数0导入文件中~
- 我的一个习惯:我会讲元素个数放在文件的首位
- 文件打开了,进行标准的读取操作
- 首先拿到上机记录数据数workNumber~
- 然后获得第一份数据~
- 后续以尾插的形式进行插入~
- 后续应该判断第一个节点是否是有效数据~
- 重点:要将尾节点的后驱置为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 补充:注销导致的强制下机计费
- 前面下机导致的注销,信息已记录
- 但是注销导致的下机,尚未解决(下面操作出现在卡管理的注销操作中~)
-
导入消费单表
-
记录强制下机消费记录
-
释放更新消费单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 测试
- 卡的情况
- 测试结果正常~
- 至于消费记录以及上机记录,再查询统计的时候一起测试~
- 二进制文件显示正常~
5. 查询统计
- 来到最后一个环节啦啦啦~ ==> 查询统计
-
- 查询单一卡一段时间的消费
- 查询全部卡一段时间额总消费
- 查询近一年来每个月的营销额以及上机次数~
-
- 引出一个新的源文件:SearchStatistics.c
- 源码在这:网吧管理系统/SearchStatistics.c · 游离态
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去导入消费记录链表
- 通过消费记录链表节点的时间属性去判断是否属于该时间段
- 若属于且是对应卡,则以表格打印出来~
- 该行用箭头指向右侧的结算时间~
- 打印思路:(打印的同时用中间寄存器释放空间)
- 如何对齐请看上文~
- 若属于且是对应卡,则以表格打印出来~
-
找不到报无~
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 细讲判断时间段方法~
- 调用transfer,将选择的数字转化为对应时间戳(一个单位时间)
- 如果满足要求且是对应的那张卡,打印下来~
5.4 查询所有卡一段时间的总消费
statisticsTime
(SearchStatistics.c)
- 通过菜单选择一个时间段
- 调用consumeCarry去导入消费记录链表
- 引入新的局部变量sum,计算总营业额~
- 通过消费记录链表节点的时间属性去判断是否属于该时间段
- 若属于,则以表格打印出来~
- 该行用箭头指向右侧的结算时间~
- 打印思路:(打印的同时用中间寄存器释放空间)
-
如何对齐请看上文~
-
并在最后,打印该时间段的总营业额~
-
- 若属于,则以表格打印出来~
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");
}
5.5 查询统计近一年来每个月的营销额以及上机次数~
statisticsMonths
(SearchStatistics.c)
- 别看代码多,其实模块化很明确~
- 后面细腻分析~
- 大致思路是
- 通过计算获取现在是第几年第几个月
- 然后推导出近12个月
- 通过十二个月每一个特定的时间戳范围查询统计划分~
- 导入消费记录和上机数据记录~
- 遍历多次出打印结果~
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;
- 年
- 月
- 此年此月的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} };
- 平年 – 2月 28天~
- 闰年 – 2月 29天~
5.5.3 判断闰年函数以及判断年月函数~
-
judgeLeapYear
(Searchstatistics.c) -
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~
-
这里用到返回结构体变量的技巧去返回两个值~
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");
- 后续打印方式还是这样:
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 测试~
- 程序运行正常~
文章到此结束!谢谢观看
可以叫我 小马,我可能写的不好或者有错误,但是一起加油鸭🦆!这是我的代码仓库!(在马拉圈的23.3里)马拉圈2023年三月: 大学生代码仓库
全部源码具体位置:网吧管理系统 . 游离态
邮箱:2040484356@qq.com