likes
comments
collection
share

【开发】打卡统计项目

作者站长头像
站长
· 阅读数 12

项目是23年的一个练手小项目,因为环节很完整,从头到尾思考了很多细节,完成后很有成就感。趁过年整理出来,尽量规范化的把整个流程梳理出来,为今后开发培养习惯,希望各位多指教。

项目背景

打卡机生成固定格式的数据,每个月会保存为一个Excel文件,记录了员工打卡的时间点。数据格式如下表:

考勤序号姓名工号所属部门5-4 四5-5 五5-6 六5-8 一...
1张三42A07:40 11:17 17:1007:44 11:19 17:4407:14 11:1907:41 14:51 17:09...
2李四53B07:37 08:48 11:12 14:59 16:5507:48 11:08 14:47 16:4507:33 11:18 14:52 16:4508:03 11:14 15:02 17:01...

需求方的打卡规则如下:

打卡需在指定的四个时间段内完成,同一时段打卡多次也只记一次
	1. 06:50 ~ 09:10 
	2. 11:10 ~ 12:30
	3. 13:40 ~ 15:10
	4. 16:29 ~ 18:01

要求设计程序,统计员工每月打卡次数。


需求分析

功能性需求

  1. 数据读写
    • Excel格式化读取,提取有效信息
    • 时间信息的存储格式及比较方法
  2. 打卡次数统计
    • 每人每日数据统计
    • 每人整月数据求和

非功能性需求

  1. 可用性需求
    • 运行环境为Windows 7或Windows 10系统
    • 面向无代码开发经验的使用者
  2. 操作需求
    • 打卡计算时间段可修改
    • 时间段个数可修改

设计思路

语言、环境选择

最开始需求方打算自己配环境跑代码,鉴于功能需求比较简单,没有性能要求,选择Python进行开发。后来对方提出Python环境配置有困难,不要求源码,所以最后用pyinstaller打包成exe交付。

相关库

  • pandas:用于Excel读写、数据格式化存储,杀鸡用牛刀,但真香
  • json:用于读取参数配置文件

参数读取

配置文件内容

clock_in_lists :打卡规则,要保证时间段成对出现,不同时间段不重合。每个时间段的开始在左,结束在右,从小到大以此排列。 num_skip_cols:跳过表格前面的列数。 in_file_nameout_file_name:输入数据文件名和输出数据文件名。

{
    "clock_in_lists": [
        "06:50",
        "09:10",
        "11:10",
        "12:30",
        "13:40",
        "15:10",
        "16:30",
        "18:00"
    ],
    "num_skip_cols": 4,
    "in_file_name": "data.xlsx",
    "out_file_name": "out.xlsx"
}

读取代码

import json as js
import pandas as pd 
# 配置文件加载
f = open('config.json', 'r')
content = f.read()
root = js.loads(content)
CLOCK_IN_LISTS = root['clock_in_lists']
NUM_SKIP_COLS = root['num_skip_cols']
# 输入/输出文件名可以改,但不能包含中文
IN_FILE_NAME = root['in_file_name']
OUT_FILE_NAME = root['out_file_name']

数据存储

文件读取一行代码即可解决,需要思考的是如何处理每个Excel格子中的内容。原始数据输入后是一个字符串,时间段之间用\n间隔,即如下格式

07:40\n11:56\n15:06\n16:38

考虑到后面有比较大小的要求,这里首先利用\n将时间段互相隔开,再把时间段转换为分钟数,用于后续比较,代码如下

# 时间段分割
item = raw_data.loc[i][j]
if pd.isna(item):
    continue
one_day_str = item.split('\n')
# 时间字符串 --> 分钟数
def time2nmin(time_str):
    time_array = time_str.split(':')
    result = int(time_array[0])*60+int(time_array[1])
    return result

打卡规则处理△

打卡规则可以简单粗暴地用if-else进行处理,但我猜写出来会很难看。我看到需求中说同一时段多次打卡也只算一次时,第一时间想到的是桶排序的方法。四个时间段相当于四个桶,每输入一个时间就判断是不是在四个桶里,最后计算不为空的桶有几个。 而第二个问题是如何简化判断规则,不用if-else,而是将时间的单调性和有序性充分利用。首先对四个时间段的起止点编号,线段表述该段时间内为合法打卡时间段,其余空白位置如B、D、F均为不合法打卡时间段。

【开发】打卡统计项目

合法段ACEG
左端点编号0246
右端点编号1357
段编号0123
非法段BDF
左端点编号135
右端点编号246

观察上述表格,可以总结出两条规律 1. 合法段端点编号满足“左偶右奇”; 2. 合法段编号=左/右端点编号 // 2

至此,我们可以设计如下判断流程:首先确认一个输入时间所在的时间段,然后判断该时间段的左右端点为奇数或偶数,比如左端点是奇数,则非法;左端点是偶数,则判断该端点的段编号,给相应的“桶”增加记录。 编写代码如下

# 打卡判断
# input:
#   clock_in_nmins: 打卡规则
#   tic_time: 刷卡时间
#   tags: 当日打卡标签
def check_tic(clock_in_nmins, tic_time, tags):
    for i in range(0, len(clock_in_nmins)):
        # 遍历打卡时间点
        if tic_time > clock_in_nmins[i]:
            continue
        else:
            if i % 2 == 1:
                tags[i // 2] = 1
            break

调试过程

边界检验

设计过程中的思路很清晰,但是实现过程中还要检验边界条件,比如小于最小的时间和大于最大时间的情况能否处理?卡点06:50的打卡,是否计算在内?第一个问题经过验证发现当前逻辑即可解决,第二个问题则需要加以讨论。当前的代码逻辑是先遍历找到右端点,然后检验其他内容。找右端点时,在输入时间≤某时间点时才停下,这种逻辑本质上是把数轴分割成了一系列左开右闭的区间(解释思路源于代码随想录的一期视频)。但是需求方的打卡分割方式其实是四个合法段是闭区间,其他非法段是开区间,可以借助下图理解。 【开发】打卡统计项目 解决的方案就是在加载打卡规则时,将左端点的时间-1,这样尽管还是左开右闭的分割,但实现了合法段形成闭区间的要求。

clock_in_nmins = []
clock_in_list_len = len(clock_in_lists)
# 此处的减1与打卡统计算法边界条件有关
for i in range(0,clock_in_list_len):
	if i % 2 == 0:    
		clock_in_nmins.append(time2nmin(clock_in_lists[i]) - 1)
	else:
		clock_in_nmins.append(time2nmin(clock_in_lists[i]))

其他问题

调试中还遇到一个由于语言特性导致的问题,第一个版本的check_tic()函数中的continuei = i+1。这个写法如果奏效,逻辑上其实也存在问题,但是这个写法在Python中实际并无作用。对 i值进行的修改只能在该轮循环中有效,下一循环i会被for语句指定的下标变化规则重置。其实原因从语义上也好理解,for i in range(0,n)就是让i遍历去取range中的值的,根本不存在+1的逻辑,也无从保留这种修改。


最终效果

使用pyinstaller打包成exe,和输入文件、配置文件放在同一文件夹下,双击运行即可。最终输出效果如图

【开发】打卡统计项目

交付内容也非常简单,exe和文档

.
|-- config.json
|-- data.xlsx
|-- exe使用说明.docx
|-- record.exe
`-- out.xlsx


复盘&改进

开发过程中应该在最开始对核心功能构建单元测试,但是还是print调试法,今后有待改进。