3. 计费管理
接下来是比较复杂的两个环节,计费管理以及查询统计~
计费管理操作上机下机,下机的时候计费~
需求:
记录上机记录
记录下机消费记录
这些记录就是为了查询统计而生的~
引入新的源文件:ChargeManage.c
源码在这:网吧管理系统/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 测试~
程序运行正常~