C语言-打卡机
一.功能
1.上班打卡
2.下班打卡
3.设置每日工作时长
4.测试需要6s=1h
5.弹性打卡制
6.周报
二.整体构思
- 根据题目要求,发现如果不使用数据库就不太容易实现全部功能,也有想过将数据通过结构体储存,并且输出到自定义格式的文件中,给他写个load和saveAs,但这样比数据库更麻烦。所以最终选择用sqlite3来做。
- 数据库每周会新建一张表格,列:id,姓名,周一到周五上下班时间。(测试模式不新建,默认打开test表格)。
i. Id储存方式:int工号
ii. 上下班时间储存方式:由于上下班时间只需要记录在一天中的时刻就可以,所以选择将“从当天0:00到打卡时刻的秒数”以int方式储存,方便计算与转换。 - 总结一下需要封装的数据库操作:打开、关闭数据库,增加员工,上下班打卡,查询id是否存在,将当前表格格式化输出,打印周报。
- 时间:由于测试需要6秒代表一个小时,而且<time_h>里面的函数并不太好用,所以新建一个用户自定义的时间查询函数库,需要有以下功能:
i. 初始化:记录打卡机开启时间,并返回离自动关闭剩余的时间(秒),方便计时线程自动kill掉打卡机线程。
ii. 查询时间:“从当天0:00到打卡时刻的秒数”,用于打卡的时候写入数据库。
iii. 查询时间:当日是星期几,用于打卡的时候写入数据库。
三.自定义时间
发布版本比较简单,就是个简单的查询。之于测试版,只需要记录初始化时间,以这个时间作为周一早上7点,查询函数只需要计算当前时间和记录的初始化时间之间的差值,就可以得出当前的时刻。具体如下:
usr_time.h:
#ifndef __USR_TIME__
#define __USR_TIME__
int usr_time_Init(void); //初始化时间,返回离自动关闭剩余剩余的秒数
int usr_time_GetDayInWeek(); //获取当前星期,1-7
int usr_time_GetSecInDay(); //获取当前时刻的秒数(从当日0:00起)
#endif
usr_time.c:
#include "usr_time.h"
#include <time.h>
#include <stdio.h>
#include <unistd.h>
#include "main.h"
#ifdef __TEST__
time_t start_timet;
int usr_time_Init(void){//返回离自动关闭剩余的秒数
time(&start_timet);
return 6*(24*5+5);//6秒等于一小时,共5天零5小时。
}
time_t time_rt;
int usr_time_GetDayInWeek(){
time(&time_rt);
int res = (int)((time_rt-start_timet+42)/144)+1;
res = (res<1)?1:((res>5)?5:res);
return res;
}
int usr_time_GetSecInDay(){
time(&time_rt);
return ((time_rt-start_timet+42)%144)*600;
}
#else
time_t a ;
struct tm* b;
int usr_time_Init(void){//返回离自动关闭剩余的秒数
time(&a);
b = localtime(&a);
if(b->tm_wday>5){
return (12-(b->tm_wday))*86400+(24-(b->tm_hour)*3600)+(60-(b->tm_min)*60+(b->tm_sec));
}
return (5-(b->tm_wday))*86400+(86400-usr_time_GetSecInDay());
}
int usr_time_GetDayInWeek(){
time(&a);
return localtime(&a)->tm_wday;
}
int usr_time_GetSecInDay(){
time(&a);
b = localtime(&a);
return (b->tm_hour)*3600+(b->tm_min)*60+(b->tm_sec);
}
#endif
四.Sqlite3数据库基本操作(增删改查)及其封装形式
Sqlite3的需要稍微掌握一下数据库的知识和操作语句。稍微复习一下增删改查:
增:
insert into <tab Name> values(xx,xx,x,xx,xx)
删:
delete from <tab Name> where id=xx
改:
update <tab Name> set <column Name>=xx where id=xx
查:
select * from <tab Name>
然后就是通过各种字符串操作,将这些常用的功能封装好,方便使用。
usr_sql3.h:
#ifndef __USR_SQL3_H__
#define __USR_SQL3_H__
/* 封装了一些数据库操作 ,分别是:
* 打开、关闭数据库 sql_OpenDB sql_CloseDB
* 检查id是否存在 sql_IsIdExit
* 打开表格 sql_OpenOrCreatTab
* 新建一行数据 sql_NewLine
* 删除一行数据 sql_DelLine
* 打卡 sql_clock
* 打印当前表格 sql_showAll
* 从当前表格生成并打印周报表 sql_showWeekReport
*/
void sql_OpenDB(const char * address);
void sql_CloseDB(void);
int sql_IsIdExit(int id);
void sql_OpenOrCreatTab(const char* address);
void sql_NewLine(int id, char* name);
void sql_DelLine(int id);
void sql_clock(int id,int dayInWeek,int secondInADay, int in1_out2, int min_workHour);
void sql_showAll(void);
void sql_showWeekReport(int min_WorkHour);
#endif
usr_sql3.c:
#include <sqlite3.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include "usr_sql3.h"
#include "main.h"
static sqlite3 * db;
static char* tabname_using;
static char *errorMsg;
void sql_OpenDB(const char* address){
sqlite3_open(address,&db);
}
void sql_CloseDB(void){
int res = sqlite3_close(db);
}
int sql_IsIdExit(int id){
char* final = (char*)malloc(200);
sprintf(final,"select * from %s",tabname_using);
int nrow,ncolumn;
char ** db_result;
int res = sqlite3_get_table(db,final,&db_result,&nrow,&ncolumn,&errorMsg);
int id_sele=0;
for(int i=0;i<(nrow+1)*ncolumn;i+=ncolumn)
{
sscanf(db_result[i],"%d",&id_sele);
if(id_sele==id){
return i;
}
}
return 0;
}
void sql_OpenOrCreatTab(const char* tabname){
char* s1 = "create table if not exists ";
char* s3 = "(id int(6),name char(20),mon_in int(20),mon_out int(20),tue_in int(20),tue_out int(20),wed_in int(20),wed_out int(20),thu_in int(20),thu_out int(20),fri_in int(20),fri_out int(20))";
char* final = (char*)malloc(strlen(tabname)+strlen(s1)+strlen(s3));
sprintf(final,"%s%s%s",s1,tabname,s3);
sqlite3_exec(db,final,0,0,&errorMsg);
tabname_using = (char* )tabname;
}
void sql_NewLine(int id,char* name){
char final[200];
sprintf(final,"insert into %s values(%d,'%s',90000,90000,90000,90000,90000,90000,90000,90000,90000,90000)",tabname_using,id,name);
sqlite3_exec(db,final,0,0,&errorMsg);
}
void sql_DelLine(int id){
char final[200];
sprintf(final,"delete from %s where id=%d",tabname_using,id);
sqlite3_exec(db,final,0,0,&errorMsg);
}
void sql_clock(int id,int dayInWeek,int secondInADay, int in1_out2 ,int min_workHour){
char week[5];
switch (dayInWeek)
{
case 1: strcpy(week, "mon_"); break;
case 2: strcpy(week, "tue_"); break;
case 3: strcpy(week, "wed_"); break;
case 4: strcpy(week, "thu_"); break;
case 5: strcpy(week, "fri_"); break;
default:return;
}
char final[200];
if(in1_out2==1){
sprintf(final,"update %s set %s%s=%d where id=%d",tabname_using,week,"in",secondInADay,id);
sqlite3_exec(db,final,0,0,&errorMsg);
return;
}
sprintf(final,"update %s set %s%s=%d where id=%d",tabname_using,week,"out",secondInADay,id);
//查询今天的上班打卡时间,并计算时常
char select_query[100];
sprintf(select_query,"select * from %s",tabname_using);
int nrow,ncolumn;
char ** db_result;
sqlite3_get_table(db,select_query,&db_result,&nrow,&ncolumn,&errorMsg);
int clkin_timep;
sscanf(db_result[2*dayInWeek+sql_IsIdExit(id)],"%d",&clkin_timep);
if (clkin_timep==90000){
printf("您今日没有上班打卡!\n");
return;
}
sqlite3_exec(db,final,0,0,&errorMsg);
printf("%d:下班打卡成功\n",id);
int hour_ = (secondInADay - clkin_timep)/3600;
int min = ((secondInADay - clkin_timep)%3600) / 60;
printf("您今天的工作时长是:%d小时%d分\n",hour_,min);
if (hour_>=min_workHour)
return;
else
printf("您的工作时常不足,还差%d分钟\n",(min_workHour*3600-(secondInADay-clkin_timep))/60);
}
void sql_showAll(void){
char select_query[100];
sprintf(select_query,"select * from %s",tabname_using);
int nrow,ncolumn;
char ** db_result;
sqlite3_get_table(db,select_query,&db_result,&nrow,&ncolumn,&errorMsg);
int i,j;
int sec;
for(i=0;i<(nrow+1)*ncolumn;i+=ncolumn)
{
for(j=0;j<ncolumn;j++)
{
if(j>1&&i>0){
sscanf(db_result[i+j],"%d",&sec);
if(sec>86400){
printf("无\t");
}else{
printf("%d:%d\t",sec/3600,(sec%3600)/60);
}
}else{
printf("%s\t",db_result[i+j]);
}
}
printf("\n");
}
}
void sql_showWeekReport(int min_WorkHour){
char select_query[100];
sprintf(select_query,"select * from %s",tabname_using);
int nrow,ncolumn;
char ** db_result;
sqlite3_get_table(db,select_query,&db_result,&nrow,&ncolumn,&errorMsg);
printf("\n\
**************************************************************\n\
** 周报! **\n\
**************************************************************\n\
姓名:\t\t平均时长:\t缺卡次数:\t工时不足次数:\n");
for(int i = ncolumn; i < (nrow+1) * ncolumn; i += ncolumn){
int in[5]={90000,90000,90000,90000,90000};
int out[5]={90000,90000,90000,90000,90000};
for(int m = 2;m<12;m++){
sscanf(db_result[i+m],"%d",(m%2==0)?(in+(m-2)/2):(out+(m-3)/2));
}
int sum=0;
int lossTimes =0;
int NotEnoughTimes =0;
for(int index = 0;index<5;index++){
if(in[index]!=90000 && out[index]!=90000){
int dayWorkSec = out[index]-in[index];
sum+= dayWorkSec;
if(dayWorkSec<min_WorkHour*3600) NotEnoughTimes++;
continue;
}
NotEnoughTimes++;
if(in[index]==90000) lossTimes++;
if(out[index]==90000) lossTimes++;
}
char* name = db_result[i+1];
int aver_h = sum/5/3600;
int aver_m = ((sum/5)%3600)/60;
printf("%s\t\t%d时%d分\t\t%d\t\t%d\n",name,aver_h,aver_m,lossTimes,NotEnoughTimes);
}
printf("\n\n\n");
}
五.打卡机:多线程同时实现定时自动退出和用户主动退出
做到这个功能需要使用三个线程:
主线程:开启“自动退出计时”线程,和“真正的打卡机线程”,然后阻塞等待“真正的打卡机线程”结束,但是打卡机线程结束可能是用户主动选择退出,所以需要在“真正的打卡机线程”结束之后,kill掉“自动退出计时”线程。
自动退出计时:需要在某个时刻杀掉打卡机线程,所以“自动退出”线程很简单,就是暂停一段时间,然后kill掉“打卡机线程”。
打卡机线程:一个大循环阻塞式接收输入,然后switch到对应的功能。
代码如下:
clockin_machine.h:
#ifndef __CLOCKIN_MACHINE__
#define __CLOCKIN_MACHINE__
void clockin_machine_start(void);
void TrueClkInMachine(void);
void DeadLineShutDown(void);
void ClockIn_local(int a);
void AddEmployee(void);
void DelEmployee(void);
void SetWorkTimePerDay(void);
void ShowWeekSummary(void);
#endif
clockin_machine.c:
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include "clockin_machine.h"
#include "usr_time.h"
#include "usr_sql3.h"
#include "sqlite3.h"
#include <unistd.h>
#include <time.h>
#include "main.h"
sqlite3 *db = 0;
pthread_t thTrueClkInMachine, thDeadLineShutDown;
int minWorkHour =9;
//进行一些打卡机的初始化工作,并控制打卡机的开启停止
void clockin_machine_start(){
int res;
char *s;
printf("clockin_machine is started!\n");
sql_OpenDB("./clk_machine.sqlite");
#ifdef __TEST__
//新建一个test空表格,列为:员工编号,姓名,周1-5上下班打卡时间
sql_OpenOrCreatTab("test");
#else
//查询当前的周,如果是数据库里没有的,就新建一个表格,以该周的周一的日子命名,如果有,就打开这个表
struct tm* tms;
time_t tt;
time(&tt);
tms = localtime(&tt);
int year = tms->tm_year;
int FirstDayOfThisWeek_InYear = (tms->tm_wday<6)?((tms->tm_yday)-(tms->tm_wday-1)):((tms->tm_yday)+8-(tms->tm_wday));
char finalTabName[20]=" ";
sprintf(finalTabName,"tab_%d%d",year,FirstDayOfThisWeek_InYear);
sql_OpenOrCreatTab(finalTabName);
printf("当前表格为:%s\n",finalTabName);
#endif
//打开真正的打卡机线程
res = pthread_create(&thTrueClkInMachine, NULL, (void *)TrueClkInMachine, NULL);
res = pthread_create(&thDeadLineShutDown, NULL, (void *)DeadLineShutDown, NULL);
//线程结束以后,打印该周总结
pthread_join(thTrueClkInMachine, NULL);
//如果计时器没有结束(用户主动推出),就杀掉计时器。
pthread_kill(thDeadLineShutDown,SIGKILL);
//展示一周总结
ShowWeekSummary();
}
//直接计算出离最终结束剩余的时间,直接延时到终结,然后shut down掉打卡机线程,结束。
void DeadLineShutDown(void){
int restTimeSec = usr_time_Init();
sleep(restTimeSec);
pthread_kill(thTrueClkInMachine,SIGKILL);
}
//打卡机,根据命令 进行功能选择。
void TrueClkInMachine(void){
system("clear");
while(1){
printf("\n\
**************************\n\
** 打卡机 **\n\
**************************\n\
1:上班打卡\n\
2:下班打卡\n\
3:添加员工\n\
4:删除员工\n\
5:设置每日上班时长\n\
6:查看周报\n\
7:结束\n\
8:浏览表格\n\
");
int chooseindex;
int res = scanf("%d",&chooseindex);
if(res ==1){//输入成功
switch (chooseindex)
{
case 1:
ClockIn_local(1);
break;
case 2:
ClockIn_local(2);
break;
case 3:
AddEmployee();
break;
case 4:
DelEmployee();
break;
case 5:
SetWorkTimePerDay();
break;
case 6:
ShowWeekSummary();
break;
case 7:
return;
case 8:
sql_showAll();
break;
default:
break;
}
}
}
}
void ClockIn_local(int in1_out2){
system("clear");
int id;
printf("输入工号:____________\b\b\b\b\b\b\b\b\b\b\b\b");
//输入六位数字工号
int res;
do{
printf("请输入六位数字!\n");
fflush(stdin);
res = scanf("%d",&id);
}while(res!=1 || id<100000 || id>999999);
//检查数据库中是否有这个工号
if(!sql_IsIdExit(id)){
printf("这个ID不存在!\n");
return;
}
//计算出校验码,(id除首位反序,并求和)
char antiid[7];
sprintf(antiid,"%d",id);
char reg;
for(int i =1;i<3;i++){
reg = antiid[i];
antiid[i]=antiid[6-i];
antiid[6-i] = reg;
}
int antiid_num;
sscanf(antiid,"%d",&antiid_num);
antiid_num+=id;
printf("%d\n请输入校验码:______\b\b\b\b\b\b",antiid_num);
int input;
scanf("%d",&input);
if(input==antiid_num){
sql_clock(id,usr_time_GetDayInWeek(),usr_time_GetSecInDay(),in1_out2,minWorkHour);
printf("%d打卡成功!\n",id);
}else{
printf("校验码输入错误!打卡失败!\n");
}
}
void AddEmployee(){
system("clear");
int id;
printf("输入工号:____________\b\b\b\b\b\b\b\b\b\b\b\b");
//输入六位数字工号
int res;
do{
printf("请输入六位数字!\n");
fflush(stdin);
res = scanf("%d",&id);
}while(res!=1 || id<100000 || id>999999);
//检查数据库中是否有这个工号
if(sql_IsIdExit(id)){
printf("这个ID已经存在!\n");
return;
}
char name[20];
printf("请输入姓名:________\b\b\b\b\b\b\b\b");
scanf("%s",name);
sql_NewLine(id,name);
}
void DelEmployee(){
system("clear");
int id;
printf("输入工号:____________\b\b\b\b\b\b\b\b\b\b\b\b");
//输入六位数字工号
int res;
do{
printf("请输入六位数字!\n");
fflush(stdin);
res = scanf("%d",&id);
}while(res!=1 || id<100000 || id>999999);
//检查数据库中是否有这个工号
if(!sql_IsIdExit(id)){
printf("这个ID不存在!\n");
return;
}
sql_DelLine(id);
}
void SetWorkTimePerDay(){
printf("请设置每日最短工作时间:____小时。\b\b\b\b\b\b");
scanf("%d",&minWorkHour);
}
void ShowWeekSummary(){
sql_showWeekReport(minWorkHour);
}
六.通过宏定义实现测试和发布两个版本
在main.h中定义一个#define TEST
然后在需要测试和发布版本不同的地方,用条件编译:
#ifdef TEST
…………
#else
…………
#endif
Main和main.h代码如下:
main.h:
//#define __TEST__
main.c:
#include <stdio.h>
#include "clockin_machine.h"
#include "main.h"
int main(){
clockin_machine_start();
}
七.makefile文件编写
makefile如何编写网上多的是,不再赘述。
这里要注意:由于sqlite3.h并不是c标准库,所要手动添加链接。最终makefile文件如下:
makefile
objects = main.o \
clockin_machine.o \
usr_time.o \
usr_sql3.o
clkmachine: $(objects)
clang $^ -o $@ -l sqlite3
rm ./*.o
main.o: main.c
clang -c main.c
clockin_machine.o: clockin_machine.c
clang -c clockin_machine.c
usr_time.o: usr_time.c
clang -c usr_time.c
usr_sql3.o: usr_sql3.c
clang -c usr_sql3.c
clean:
rm ./*.o clkmachine