Tianyi731 发表于 2024-7-29 15:14

钉钉打卡提醒

本帖最后由 Tianyi731 于 2024-7-29 17:59 编辑

## 小弟因为在一家手机公司做技术支持,偶尔需要值班,经常忘记打卡,故写了这个工具,发出来记录一下,顺便让大佬们看看有没有可以优化的地方。
# 准备工作
云服务器(本地小型服务器)、一台有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('-')), int(date.split('-')), int(date.split('-'))
      
      # 检查年份是否在字典中,如果不在则创建
      if year not in json_data:
            json_data = {}
      
      # 检查月份是否在年份字典中,如果不在则创建
      if month not in json_data:
            json_data = {}
      
      # 将班次添加到字典中
      json_data = 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 = {}
      for month, days in months.items():
            if str(month) not in existing_data:
                existing_data = days
            else:
                for day, shift in days.items():
                  if str(day) in existing_data:
                        print(f"Warning: Data for {year}-{month}-{day} already exists.")
                        response = input(f"是否覆盖此条数据? (y/n): ")
                        if response.lower() == 'y':
                            existing_data = shift
                  else:
                        existing_data = shift
   
    # 将数据按照年、月、日的顺序排序
    sorted_data = {}
    for year in sorted(existing_data.keys(), key=int):
      sorted_data = {}
      for month in sorted(existing_data.keys(), key=int):
            sorted_data = existing_data
   
    # 将更新后的数据写回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也可以,只是比较麻烦。

纤细的企鹅 发表于 2024-8-2 17:27

大佬牛的。但是似乎有更方便的,就是手机的自带的自动任务,小米手机是有的,华为也有,其他手机不清楚。设置出发条件,比如到XX时间,连接蓝牙之后,弹出钉钉之类的,除了定位有偏差以外,总体来说,我设置任务之后没有漏过卡

zhoutong8866 发表于 2024-7-29 16:57

看着很高级的样子?一点也不懂,留给专业人士玩,{:1_927:}

yrycw 发表于 2024-7-29 16:58

全是代码,小白也看不懂,楼主厉害了

stormeidolon 发表于 2024-7-29 16:59

非程序人员看不懂,留给需要的人。

wodekuxiao1025 发表于 2024-7-29 17:12

搞这么多P用,直接远程一部公司的手机不香吗

dyk03150088 发表于 2024-7-29 17:15

钉钉打卡有时候忘了,还要被领导PUA,难顶

海是倒过来的天 发表于 2024-7-29 17:30

wodekuxiao1025 发表于 2024-7-29 17:12
搞这么多P用,直接远程一部公司的手机不香吗

这个讨论的不是远程打卡,是忘记打卡。远程的前提是你要记得远程

exitfang 发表于 2024-7-29 17:48

jian1098 发表于 2024-7-29 17:55

手机定个日程或者闹钟不就好了吗

Tianyi731 发表于 2024-7-29 17:58

jian1098 发表于 2024-7-29 17:55
手机定个日程或者闹钟不就好了吗

我一开始也是这么想的,但是定1个闹钟有时候不满足打卡条件,比如差几分钟到公司,还有我上班的日期不是那么有规律的,所以一个个定闹钟还麻烦,有时候顺手取消闹钟回头又忘了
页: [1] 2 3 4 5
查看完整版本: 钉钉打卡提醒