小弟因为在一家手机公司做技术支持,偶尔需要值班,经常忘记打卡,故写了这个工具,发出来记录一下,顺便让大佬们看看有没有可以优化的地方。
准备工作
云服务器(本地小型服务器)、一台有xp框架的手机(没有也行,但是只能通知,无法实现全流程自动化)
服务器我用的是50块钱收的M401A电视盒子,刷的armbian。
第一步
使用python代码转换班次为json数据,表格格式以此类推
时间 |
班次 |
2024/8/1 |
A |
2024/8/1 |
P |
2024/8/1 |
休 |
#本脚本作用为读取表格数据,写入json,表格模版为schedule.xlsx
import pandas as pd
import json
from datetime import datetime
def read_excel_and_convert_to_json(excel_file):
# 读取Excel文件
df = pd.read_excel(excel_file, engine='openpyxl')
# 创建一个空字典来存储JSON数据
json_data = {}
# 获取日期和班次列
date_column = df['日期']
shift_column = df['班次']
# 遍历每一行数据
for index, row in df.iterrows():
date = row['日期'].strftime('%Y-%m-%d') # 确保日期是字符串格式
shift = row['班次']
# 拆分日期为年、月、日,并去掉前导零
year, month, day = int(date.split('-')[0]), int(date.split('-')[1]), int(date.split('-')[2])
# 检查年份是否在字典中,如果不在则创建
if year not in json_data:
json_data[year] = {}
# 检查月份是否在年份字典中,如果不在则创建
if month not in json_data[year]:
json_data[year][month] = {}
# 将班次添加到字典中
json_data[year][month][day] = shift
return json_data
def update_json_file(json_data, json_file):
# 读取现有的JSON文件,如果文件不存在,则返回空字典
try:
with open(json_file, 'r', encoding='utf-8') as file:
existing_data = json.load(file)
except (FileNotFoundError, json.JSONDecodeError):
existing_data = {}
# 合并新数据和旧数据
for year, months in json_data.items():
if str(year) not in existing_data:
existing_data[str(year)] = {}
for month, days in months.items():
if str(month) not in existing_data[str(year)]:
existing_data[str(year)][str(month)] = days
else:
for day, shift in days.items():
if str(day) in existing_data[str(year)][str(month)]:
print(f"Warning: Data for {year}-{month}-{day} already exists.")
response = input(f"是否覆盖此条数据? (y/n): ")
if response.lower() == 'y':
existing_data[str(year)][str(month)][str(day)] = shift
else:
existing_data[str(year)][str(month)][str(day)] = shift
# 将数据按照年、月、日的顺序排序
sorted_data = {}
for year in sorted(existing_data.keys(), key=int):
sorted_data[str(year)] = {}
for month in sorted(existing_data[str(year)].keys(), key=int):
sorted_data[str(year)][str(month)] = existing_data[str(year)][str(month)]
# 将更新后的数据写回JSON文件
with open(json_file, 'w', encoding='utf-8') as file:
json.dump(sorted_data, file, ensure_ascii=False, indent=2)
# Excel文件路径
excel_file = 'schedule.xlsx'
# JSON文件路径
json_file = 'data.json'
# 读取Excel文件并转换为JSON数据
new_json_data = read_excel_and_convert_to_json(excel_file)
# 更新JSON文件
update_json_file(new_json_data, json_file)
转换的json格式
{
"2023": {
"12": {
"1": "A",
"2": "A",
"3": "A",
"4": "A",
"5": "A",
"6": "A",
"7": "A",
"8": "A",
"9": "A",
"10": "A",
"11": "A",
"12": "A",
"13": "A",
"14": "A",
"15": "A",
"16": "A",
"17": "A",
"18": "A",
"19": "A",
"20": "A",
"21": "A",
"22": "A",
"23": "A",
"24": "A",
"25": "A",
"26": "A",
"27": "A",
"28": "A",
"29": "A",
"30": "A",
"31": "A"
}
},
"2024": {
"7": {
"1": "A",
"2": "A",
"3": "A",
"4": "A",
"5": "A",
"6": "A",
"7": "A",
"8": "A",
"9": "A",
"10": "A",
"11": "A",
"12": "A",
"13": "A",
"14": "A",
"15": "A",
"16": "A",
"17": "A",
"18": "A",
"19": "A",
"20": "A",
"21": "A",
"22": "A",
"23": "A",
"24": "A",
"25": "A",
"26": "A",
"27": "A",
"28": "A",
"29": "A",
"30": "A",
"31": "A"
},
"8": {
"1": "A",
"2": "A",
"3": "A",
"4": "A",
"5": "A",
"6": "A",
"7": "A",
"8": "A",
"9": "A",
"10": "A",
"11": "A",
"12": "A",
"13": "A",
"14": "A",
"15": "A",
"16": "A",
"17": "A",
"18": "A",
"19": "A",
"20": "A",
"21": "A",
"22": "A",
"23": "A",
"24": "A",
"25": "A",
"26": "A",
"27": "A",
"28": "A",
"29": "A",
"30": "A",
"31": "A"
}
}
}
云端代码
服务器代码
import json
import time
from datetime import datetime, timedelta
import requests
import os
print("Current working directory:", os.getcwd())
api_token = '你的金山文档表格令牌'
url = "你的金山文档表格webhook地址"
# 设置请求头部
headers = {
'Content-Type': 'application/json',
'AirScript-Token': api_token
}
def working(): # 上班通知
url = "你的通知webhook"
requests.post(url)
def no_work(): # 下班通知
url = "你的通知webhook"
requests.post(url)
def read_schedule(file_path):
# 读取 JSON 文件并返回解析后的数据。
with open(file_path, 'r', encoding='utf-8') as file:
schedule = json.load(file)
return schedule
def get_shifts_for_date(schedule, year, month, day):
# 根据指定的日期获取班次。
return schedule.get(str(year), {}).get(str(month), {}).get(str(day), "休")
def print_shifts(shift):
# 根据班次输出上班和下班的时间提示,并设置 state 变量。
now = datetime.now() # 获取当前时间
current_time_obj = datetime.strptime(now.strftime("%H:%M"), "%H:%M").time() # 当前时间转换为 time 对象
state = "none" # 初始化 state 变量为 "none",表示不在任何打卡时间
if shift == "休":
print("今天休息")
state = "rest"
else:
start_time_str = "9:00" if shift == "A" else "9:30" # 根据班次类型确定上班时间
end_time_str = "18:00" if shift == "A" else "18:30" # 根据班次类型确定下班时间
start_time = datetime.strptime(start_time_str, "%H:%M").time()
end_time = datetime.strptime(end_time_str, "%H:%M").time()
start_time_datetime = datetime.combine(now.date(), start_time)
end_time_datetime = datetime.combine(now.date(), end_time)
start_15_min_later_datetime = start_time_datetime - timedelta(minutes=15)# 上班前15分钟的时间
end_30_min_later_datetime = end_time_datetime + timedelta(minutes=30)# 在下班时间的基础上加上30分钟
start_15_min_later = start_15_min_later_datetime.time()# 将结果转换回 time 对象
end_30_min_later = end_30_min_later_datetime.time()# 将结果转换回 time 对象
print(f"60-当前时间:{current_time_obj}")#当前时间
print(f"61-上班时间:{start_time}")#上班时间
print(f"62-上班开始时间:{start_15_min_later}")#上班开始通知时间
print(f"63-下班时间:{end_time}")#下班时间
print(f"64-下班最后时间:{end_30_min_later}")#下班最后通知时间
# 如果当前时间在上班前15分钟至上班时间之间,则调用通知函数,设置 state 为 "work"
if start_15_min_later <= current_time_obj < start_time:
state = "work"
print("当前在上班时间")
# 如果当前时间在下班时间至下班后30分钟之间,则调用通知函数,设置 state 为 "worked"
elif end_time < current_time_obj <= end_30_min_later:
state = "worked"
print("当前在下班时间")
else:
print("74-当前不在可通知时间")
return state
if __name__ == "__main__":
schedule = read_schedule("你的服务器json文件路径") # 确认JSON文件
today = datetime.now().strftime("%Y-%m-%d") # 获取今天的日期
year, month, day = map(int, today.split("-")) # 分别获取年、月、日
shifts = get_shifts_for_date(schedule, year, month, day) # 获取今天的班次
print(f"84-当前班次: {shifts}")
state = print_shifts(shifts) # 根据班次信息和当前时间确定 state
print(f"86-当前时间状态: {state}") # 打印当前状态
if state == "work":
message = "A1"
elif state == "worked":
message = "A2"
else:
message = "A3"
print(f"94-当前表格范围状态:{message}")
#设置请求体,包含单元格地址参数
payload = {
"Context": {
"argv": {
"message": message
},
}
}
response = requests.post(url, headers=headers, json=payload)#发出请求
result_data = response.json()#读取请求返回json
task_result = result_data.get('data', {}).get('result', 'No result data')#读取打卡检测值
check_value = task_result.get('打卡检测')
print(f"108-是否已打卡:{str(check_value)}")
if check_value == "上班未打卡":
working()
elif check_value == "下班未打卡":
no_work()
else:
print("114-当前不符合")
金山文档airscript代码
我们需要两个airscript脚本,第一个实现读取当前是否已经打卡,第二个实现打卡后修改表格数据为已打卡,第二个脚本需要使用金山文档定时脚本功能,定时23:30修改表格值为未打卡,为明天的打卡做准备
//此脚本的作用为读取表格值,返回到python中
const message = Context.argv.message || "A3";
//const message = "A3"
const range = ActiveSheet.Range(message)
// 读取范围内的值
const value = range.Value2 // 输出值
console.log(value)
Context.argv.X=value
return {"打卡检测": value};
//此脚本的作用是,手机打卡后,触发webhook,修改表格值为上班/下班已打卡
function A(){
const message = "A1"
const data = "上班未打卡"
const range = ActiveSheet.Range(message)
range.Value2 = data
}
function B(){
const message = "A2"
const data = "下班未打卡"
const range = ActiveSheet.Range(message)
range.Value2 = data
}
const data = Context.argv.data || "初始化";
if (data == "初始化") {
console.log(data);
A();
B();
}
else if (data == "上班打卡") {
const message = Context.argv.message
const range = ActiveSheet.Range(message)
range.Value2 = "上班已打卡"
return {"打卡检测": range.Value2};
}
else if (data == "下班打卡") {
const message = Context.argv.message
const range = ActiveSheet.Range(message)
range.Value2 = "下班已打卡"
return {"打卡检测": range.Value2};
}
金山文档A1为上班打卡检测,A2为下班打卡检测。这两个格子的值为上班/下班已打卡;上班/下班未打卡。
手机端代码
打卡后使用shell脚本修改云文档数据
判断当前使用上班还是下班打卡,然后修改金山文档表格数据,逻辑交给thanox去判断,手机端代码执行的越快越好。
#!/bin/bash
# 接收thanox传入的参数
if [ "$1" = "上班打卡" ]; then
data="上班打卡"
message="A1"
elif [ "$1" = "下班打卡" ]; then
data="下班打卡"
message="A2"
fi
# 定义变量
api_token='金山文档airscript脚本令牌'
url='金山文档airscript脚本webhook地址'
# 创建请求体
payload=$(cat <<EOF
{
"Context": {
"argv": {
"message": "$message",
"data": "$data"
}
}
}
EOF
)
# 发送 POST 请求并捕获响应
curl -s -X POST -H "Content-Type: application/json" -H "AirScript-Token: $api_token" -d "$payload" "$url"
#response=$(curl -s -o response.txt -w "%{http_code}" -X POST -H "Content-Type: application/json" -H "AirScript-Token: $api_token" -d "$payload" "$url")
# 检查响应状态码
#if [ "$response" -eq 200 ]; then
# 解析JSON响应
# result_data=$(cat response.txt)
# task_result=$(echo "$result_data" | jq -r '.data.result')
# 打印结果
# echo "$result_data"
# echo "Task result: $task_result"
#else
# echo "Failed to send request, status code: $response"
#fi
# 清理临时文件
#rm response.txt
thanox 情景模式代码
这段代码的作用是监听通知,关键字“打卡咯”,即打开钉钉。
注意,这里的通知要与上面触发提醒的通知中的关键字有重合,例如我的打卡咯,标题为打卡提醒,正文根据上下班为上班/下班打卡咯,在情景模式代码中,我就设置关键字为打卡咯,这个咯是有存在必要的。
[
{
"name": "钉钉打卡",
"description": "当收到包含'打卡咯'关键词的通知时,自动打开钉钉应用",
"priority": 1,
"condition": "notificationAdded == true && notificationContent.contains('打卡咯')",
"actions": [
"ui.showShortToast('收到打卡通知,打开钉钉');",
"activity.launchMainActivityForPackage('com.alibaba.android.rimet')"
]
}
]
这个时候手机会自动打开钉钉,如果我们已经打卡了,那么手机会收到一个通知:xxx公司 考勤打卡:xxx时间 上班/下班打卡·成功,所以我们再使用这段代码,实现收到打卡成功的通知后,自动后台执行打卡成功的shell脚本,修改金山文档表格数据,这里我使用的监听关键词是“打卡·成功”
如果上面的监听关键词是"上班打卡”没有“咯”,那么打卡成功,钉钉发送的通知会导致重复触发打开钉钉。
[
{
"name": "上班打卡数据",
"description": "当收到包含'上班打卡·成功'关键词的通知时,执行指定目录中的Shell脚本",
"priority": 1,
"condition": "notificationAdded == true && (notificationTitle.contains('上班打卡·成功') || notificationContent.contains('上班打卡·成功'))",
"actions": [
"ui.showShortToast('上班打卡成功,触发数据上传');",
"su.exe('sh /sdcard/目录/打卡.sh 上班打卡')"
]
}
]
[
{
"name": "下班打卡数据",
"description": "当收到包含'下班打卡·成功'关键词的通知时,执行指定目录中的Shell脚本",
"priority": 1,
"condition": "notificationAdded == true && (notificationTitle.contains('下班打卡·成功') || notificationContent.contains('上班打卡·成功'))",
"actions": [
"ui.showShortToast('下班打卡成功,触发数据上传');",
"su.exe('sh /sdcard/目录/打卡.sh 下班打卡')"
]
}
]
通知webhook获取
通知我使用的是饭碗警告,新建转发规则。规则名称:打卡提醒;触发类型:webhook;变量名:message;变量来源:查询字符串;键:message;通知简述:打卡提醒;通知正文:{{message}};其他的默认即可。按照下述的webhook即可触发通知上班/下班打卡。
https://fwalert.com/你的饭碗警告webhook地址?message=上班打卡咯
https://fwalert.com/你的饭碗警告webhook地址?message=下班打卡咯
结尾
这样,我们就实现了,服务器定时循环运行打卡检测,检测到时间符合并且金山文档表格值为未打卡则发出通知,手机接收到通知自动打开钉钉,钉钉打卡后自动修改金山文档表格值。
如果你的手机没有root和xp框架,那么到提醒那一步就停止了,无法自动打卡钉钉和自动修改表格数据,其实使用adb和shizuku也可以,只是比较麻烦。