169 lines
6.0 KiB
Python
169 lines
6.0 KiB
Python
![]() |
import concurrent.futures
|
|||
|
import sys
|
|||
|
import requests
|
|||
|
from typing import List, Set, Dict, Tuple
|
|||
|
|
|||
|
class DingTalkDocsFetcher:
|
|||
|
|
|||
|
@staticmethod
|
|||
|
def get_access_token(app_key: str, app_secret: str) -> str:
|
|||
|
url = "https://oapi.dingtalk.com/gettoken"
|
|||
|
params = {
|
|||
|
"appkey": app_key,
|
|||
|
"appsecret": app_secret
|
|||
|
}
|
|||
|
resp = requests.get(url, params=params)
|
|||
|
data = resp.json()
|
|||
|
if data.get("errcode") == 0:
|
|||
|
print("✅ AccessToken 获取成功:", data.get("access_token"))
|
|||
|
return data.get("access_token")
|
|||
|
print("❌ 获取 AccessToken 失败:", data)
|
|||
|
return ""
|
|||
|
|
|||
|
@staticmethod
|
|||
|
def get_sub_departments(token: str, dept_id: int = None) -> List[Dict]:
|
|||
|
url = "https://oapi.dingtalk.com/topapi/v2/department/listsub"
|
|||
|
params = {"access_token": token}
|
|||
|
payload = {}
|
|||
|
if dept_id is not None:
|
|||
|
payload["dept_id"] = dept_id
|
|||
|
resp = requests.post(url, params=params, json=payload)
|
|||
|
data = resp.json()
|
|||
|
if data.get("errcode") == 0:
|
|||
|
return data.get("result", [])
|
|||
|
else:
|
|||
|
print(f"❌ 获取部门列表失败 dept_id={dept_id}: {data}")
|
|||
|
return []
|
|||
|
|
|||
|
@staticmethod
|
|||
|
def get_users_by_dept(token: str, dept_id: int) -> List[str]:
|
|||
|
url = "https://oapi.dingtalk.com/topapi/user/listid"
|
|||
|
params = {"access_token": token}
|
|||
|
payload = {"dept_id": dept_id}
|
|||
|
resp = requests.post(url, params=params, json=payload)
|
|||
|
data = resp.json()
|
|||
|
if data.get("errcode") == 0:
|
|||
|
return data.get("result", {}).get("userid_list", [])
|
|||
|
else:
|
|||
|
print(f"❌ 获取部门用户失败 dept_id={dept_id}: {data}")
|
|||
|
return []
|
|||
|
|
|||
|
@staticmethod
|
|||
|
def get_all_departments(token: str) -> List[Dict]:
|
|||
|
all_depts = []
|
|||
|
|
|||
|
def recurse(dept_id: int = None, parent_id: int = 0):
|
|||
|
depts = DingTalkDocsFetcher.get_sub_departments(token, dept_id)
|
|||
|
for d in depts:
|
|||
|
dept_info = {
|
|||
|
"dept_id": d["dept_id"],
|
|||
|
"name": d["name"],
|
|||
|
"parent_id": parent_id # 这里用调用时的 parent_id
|
|||
|
}
|
|||
|
all_depts.append(dept_info)
|
|||
|
recurse(d["dept_id"], parent_id=d["dept_id"]) # 递归时传当前dept_id作为父部门ID
|
|||
|
|
|||
|
recurse()
|
|||
|
return all_depts
|
|||
|
@staticmethod
|
|||
|
def get_user_detail(token: str, userid: str) -> dict:
|
|||
|
url = "https://oapi.dingtalk.com/topapi/v2/user/get"
|
|||
|
params = {"access_token": token}
|
|||
|
payload = {"userid": userid}
|
|||
|
resp = requests.post(url, params=params, json=payload)
|
|||
|
data = resp.json()
|
|||
|
if data.get("errcode") == 0:
|
|||
|
return data.get("result", {})
|
|||
|
else:
|
|||
|
print(f"获取用户详情失败: {data}")
|
|||
|
return None
|
|||
|
|
|||
|
@staticmethod
|
|||
|
def get_all_user_ids(token: str, departments: List[Dict]) -> Set[str]:
|
|||
|
"""
|
|||
|
根据部门列表,获取所有部门用户ID集合(去重)
|
|||
|
"""
|
|||
|
user_ids = set()
|
|||
|
for dept in departments:
|
|||
|
dept_id = dept["dept_id"]
|
|||
|
ids = DingTalkDocsFetcher.get_users_by_dept(token, dept_id)
|
|||
|
user_ids.update(ids)
|
|||
|
return user_ids
|
|||
|
|
|||
|
@staticmethod
|
|||
|
def get_attendance_records(token: str, user_ids: List[str], date_from: str, date_to: str):
|
|||
|
url = f"https://oapi.dingtalk.com/attendance/listRecord?access_token={token}"
|
|||
|
payload = {
|
|||
|
"checkDateFrom": date_from,
|
|||
|
"checkDateTo": date_to,
|
|||
|
"userIds": user_ids,
|
|||
|
"isI18n": "false"
|
|||
|
}
|
|||
|
resp = requests.post(url, json=payload)
|
|||
|
r = resp.json()
|
|||
|
if r.get("errcode") == 0:
|
|||
|
return r.get("recordresult", [])
|
|||
|
else:
|
|||
|
print(f"❌ 获取打卡记录失败: {r}")
|
|||
|
return []
|
|||
|
|
|||
|
@staticmethod
|
|||
|
def main(args: List[str]) -> None:
|
|||
|
app_key = "dinguetojbaxvvhzpk3d"
|
|||
|
app_secret = "lMFqns_ceLIcXvfLL8GKfa3ZiPKHcaZq0VbGtJXJlDuK8AEJ2WV3-PN8zv61ajm3"
|
|||
|
|
|||
|
token = DingTalkDocsFetcher.get_access_token(app_key, app_secret)
|
|||
|
if not token:
|
|||
|
return
|
|||
|
|
|||
|
print("开始获取所有部门...")
|
|||
|
departments = DingTalkDocsFetcher.get_all_departments(token)
|
|||
|
print(f"✅ 获取部门总数: {len(departments)}")
|
|||
|
|
|||
|
print("开始获取所有部门用户ID...")
|
|||
|
user_ids_set = DingTalkDocsFetcher.get_all_user_ids(token, departments)
|
|||
|
user_ids = list(user_ids_set)
|
|||
|
print(f"✅ 获取用户总数: {len(user_ids)}")
|
|||
|
|
|||
|
date_from = "2025-08-10 12:00:00"
|
|||
|
date_to = "2025-08-12 12:00:00"
|
|||
|
|
|||
|
print("开始获取打卡记录...")
|
|||
|
records = DingTalkDocsFetcher.get_attendance_records(token, user_ids, date_from, date_to)
|
|||
|
print(f"✅ 打卡记录数: {len(records)}")
|
|||
|
|
|||
|
user_cache = {}
|
|||
|
|
|||
|
def fetch_user_detail(userid):
|
|||
|
if userid in user_cache:
|
|||
|
return userid, user_cache[userid]
|
|||
|
detail = DingTalkDocsFetcher.get_user_detail(token, userid)
|
|||
|
user_cache[userid] = detail
|
|||
|
return userid, detail
|
|||
|
|
|||
|
user_ids_in_records = list({r.get("userId") for r in records if r.get("userId")})
|
|||
|
|
|||
|
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
|
|||
|
futures = [executor.submit(fetch_user_detail, uid) for uid in user_ids_in_records]
|
|||
|
for future in concurrent.futures.as_completed(futures):
|
|||
|
uid, detail = future.result()
|
|||
|
user_cache[uid] = detail
|
|||
|
|
|||
|
enhanced_records = []
|
|||
|
for r in records:
|
|||
|
userid = r.get("userId")
|
|||
|
user_detail = user_cache.get(userid)
|
|||
|
if user_detail:
|
|||
|
r["user_name"] = user_detail.get("name", "")
|
|||
|
else:
|
|||
|
r["user_name"] = ""
|
|||
|
enhanced_records.append(r)
|
|||
|
|
|||
|
for r in enhanced_records:
|
|||
|
print(r)
|
|||
|
|
|||
|
|
|||
|
|
|||
|
if __name__ == '__main__':
|
|||
|
DingTalkDocsFetcher.main(sys.argv[1:])
|