| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411 |
- import requests
- import json
- from datetime import datetime, timezone, timedelta
- import tkinter as tk
- from tkinter import ttk, messagebox, filedialog
- import pandas as pd
- import os
- from threading import Thread
- def parse_response(response_text):
- """
- 从飞书多维表格API响应中提取指定字段数据
- """
- try:
- # 1. 解析外层JSON
- if isinstance(response_text, dict):
- outer_data = response_text
- else:
- outer_data = json.loads(response_text)
-
- # 2. 获取data中的records字符串
- records_str = outer_data.get('data', {}).get('records', '')
- if not records_str:
- return {
- '批量数据编码': None,
- '有顾问跟进': None,
- '批量创建时间': None,
- '批量创建人员': None,
- 'error': 'records字段为空'
- }
-
- # 3. 解析内层records JSON
- records_data = json.loads(records_str)
-
- # 4. 获取第一条记录的key
- record_keys = list(records_data.get('recordMap', {}).keys())
- if not record_keys:
- return {
- '批量数据编码': None,
- '有顾问跟进': None,
- '批量创建时间': None,
- '批量创建人员': None,
- 'error': 'recordMap为空'
- }
- first_record_key = record_keys[0]
- record = records_data['recordMap'][first_record_key]
-
- # 5. 获取fieldMap - 在 formulaInfo 里面!
- formula_info = records_data.get('formulaInfo', {})
- field_map = formula_info.get('fieldMap', {})
-
- # 6. 提取批量数据编码
- batch_code = None
- if 'fldbQDk5b7' in record:
- values = record['fldbQDk5b7'].get('value', [])
- if values and len(values) > 0:
- batch_code = values[0].get('number')
-
- # 7. 提取有顾问跟进
- has_advisor = None
- if 'fldSiIb9nZ' in field_map:
- field_value = field_map['fldSiIb9nZ'].get(first_record_key, {}).get('value')
- if field_value:
- try:
- data = json.loads(field_value)
- if data.get('data') and len(data['data']) > 0:
- has_advisor = data['data'][0]
- except:
- pass
-
- # 8. 提取批量创建时间
- batch_create_time = None
- if 'fldlzQO4tZ' in field_map:
- field_value = field_map['fldlzQO4tZ'].get(first_record_key, {}).get('value')
- if field_value:
- try:
- data = json.loads(field_value)
- if data.get('data') and len(data['data']) > 0:
- timestamp_data = data['data'][0]
- if isinstance(timestamp_data, dict) and 'value' in timestamp_data:
- timestamp_ms = timestamp_data['value']
- if timestamp_ms and isinstance(timestamp_ms, (int, float)):
- if timestamp_ms > 1000000000000:
- timestamp_sec = timestamp_ms / 1000
- else:
- timestamp_sec = timestamp_ms
- dt = datetime.fromtimestamp(timestamp_sec, tz=timezone(timedelta(hours=8)))
- batch_create_time = dt.strftime('%Y-%m-%d %H:%M:%S')
- except:
- pass
-
- # 9. 提取批量创建人员
- batch_creator = None
- if 'fldm8odlu2' in field_map:
- field_value = field_map['fldm8odlu2'].get(first_record_key, {}).get('value')
- if field_value:
- try:
- data = json.loads(field_value)
- if data.get('data') and len(data['data']) > 0:
- batch_creator = data['data'][0]
- except:
- pass
-
- return {
- '批量数据编码': batch_code,
- '有顾问跟进': has_advisor,
- '批量创建时间': batch_create_time,
- '批量创建人员': batch_creator
- }
-
- except Exception as e:
- return {
- '批量数据编码': None,
- '有顾问跟进': None,
- '批量创建时间': None,
- '批量创建人员': None,
- 'error': f'处理错误: {str(e)}'
- }
- def search_single_phone(keyword):
- """
- 查询单个手机号
- """
- try:
- payload = f'''{{\"ondemandVer\":2,\"localViewConfig\":{{\"filter\":null,\"sort\":\"[{{\\\"fieldId\\\":\\\"fldSiIb9nZ\\\",\\\"desc\\\":false}},{{\\\"fieldId\\\":\\\"fldbQDk5b7\\\",\\\"desc\\\":true}}]\",\"group\":\"[]\"}},\"ignoreRecordsNum\":false,\"groupByInfoList\":\"[{{\\\"id\\\":0,\\\"offset\\\":0,\\\"limit\\\":200}}]\",\"globalSearch\":{{\"keyword\":\"{keyword}\",\"fieldIDs\":[\"fldbQDk5b7\",\"fldSiIb9nZ\",\"fldlzQO4tZ\",\"fldm8odlu2\",\"fld9KcFahs\",\"fldkZzg0y4\",\"fldFL9eHjq\",\"fldq9cmDqw\",\"fld0af1E3B\",\"fldbsO0apx\",\"fldlbKssB8\",\"fldczSrVSQ\"]}},\"baseEngineJointRev\":13452036,\"withExtra\":{{\"groupInfos\":true}},\"removeFmlExtra\":true}}'''
-
- url = "https://xhd.feishu.cn/space/api/v1/bitable/W83XbyQuPajhc8s4Ghsc5xF9nTb/ondemand/records/search"
- querystring = {"tableID":"tblXQQeIwMqSFSME","viewID":"vewETuuNwn","tableRev":"1871920","jointRev":"13452036"}
-
- headers = {
- "accept": "application/json, text/plain, */*",
- "accept-language": "zh-CN,zh;q=0.9",
- "content-type": "application/json",
- "context": "request_id=2YHprDoRiCNb-ee5749b7ac0c1b41e6d5e9c37292b9f07330118d;os=windows;app_version=1.0.19.9369;os_version=10;platform=web",
- "doc-biz": "Lark",
- "doc-os": "windows",
- "doc-platform": "web",
- "f-version": "docs-8-19-1787124320993",
- "origin": "https://xhd.feishu.cn",
- "priority": "u=1, i",
- "referer": "https://xhd.feishu.cn/base/W83XbyQuPajhc8s4Ghsc5xF9nTb?table=tblXQQeIwMqSFSME&view=vewETuuNwn",
- "request-id": "2YHprDoRiCNb-ee5749b7ac0c1b41e6d5e9c37292b9f07330118d",
- "rpc-persist-lane-c-lark-uid": "0",
- "sec-fetch-dest": "empty",
- "sec-fetch-mode": "cors",
- "sec-fetch-site": "same-origin",
- "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36",
- "x-csrftoken": "e58ad7674c941341cc4b663a42eed3390a50b387-1786763321",
- "x-lgw-app-id": "1161",
- "x-lgw-os-type": "1",
- "x-lgw-terminal-type": "2",
- "x-lsc-bizid": "2",
- "x-lsc-terminal": "web",
- "x-lsc-version": "1",
- "x-request-id": "2YHprDoRiCNb-ee5749b7ac0c1b41e6d5e9c37292b9f07330118d",
- "x-tt-logid": "02178759464265700000000000000000000ffff1705590efc4048",
- "x-tt-trace-id": "2YHprDoRiCNb-ee5749b7ac0c1b41e6d5e9c37292b9f07330118d",
- "Cookie": "__tea__ug__uid=4144561781538482924; _gcl_au=1.1.775810441.1781538483; passport_web_did=7651823325329411260; passport_trace_id=7651823325353757894; QXV0aHpDb250ZXh0=ba539bcfe4a042c8bc1e99d4d03add38; lang=zh; et=c0c63f4480fdf14d7e6e9a21bab2b7e1; ccm_cdn_host=//lf-package-cn.feishucdn.com/obj/feishu-static; _uetvid=9877555068d111f1992d03b61d301033; is_anonymous_session=; i18n_locale=zh-CN; i18n_locale=zh-CN; locale=zh-CN; s_v_web_id=verify_mqg251ox_Tp4ofGaR_NCf4_4zII_8Ad3_c3HxGb4PXIuN; Hm_lvt_a79616d9322d81f12a92402ac6ae32ea=1781538483,1781579751,1783595682; _csrf_token=e58ad7674c941341cc4b663a42eed3390a50b387-1786763321; js_version=1; _tea_utm_cache_8608=undefined; _tea_utm_cache_323044=undefined; _gid=GA1.2.1635584096.1787486227; _uuid_hera_ab_path_1=7677197560582245583; _ga=GA1.1.692261332.1781538484; landing_url=https://accounts.feishu.cn/accounts/page/login?app_id=2&no_trap=1&query_scope=all&redirect_uri=https%253A%252F%252Fxhd.feishu.cn%252Fapp%252FZaEWbUvjeaYgS6sof3ycpikNnwh%253Ffrom%253Dspace_home_recent%2526login_redirect_times%253D1%2526pre_pathname%253D%25252Fbase%2526previous_navigation_time%253D1787535012983%2526skip_html_cache%253D1; session=XN0YXJ0-2b4ha893-8dde-44b0-9da1-a45f033de7f3-WVuZA; session_list=XN0YXJ0-2b4ha893-8dde-44b0-9da1-a45f033de7f3-WVuZA; login_recently=1; ssrLocalKey=3b07d7d4a86ab7d7f6bfd714f3c0f3596cba82749fa9f4cc5020bfff57eaea92; msToken=3MPLenLPXfPJet02DA1uWTNf0EIiJic7Xy2mllIUmUa0vdZ1A28Iego52sEo545x7QFQX6aPwMIeNRPg2S_87YXmt6tpxndT2l5w8IXG6zerU6YqRV6Em7Kn7XZ878fm2DJmKTrc6VPvvW7XTDK5OzbY6LQ_4KIE88kVLTS38TFp; _ga_VPYRHN104D=GS2.1.s1787591886$o13$g1$t1787592091$j60$l0$h0; bitable_tableId_viewId_history=%7B%22CkXLbknSiaaRtCsbdwgciiZunge%22%3A%7B%22tableId%22%3A%22blksKA7e5zhv4YzB%22%2C%22viewId%22%3A%22%22%7D%2C%22VFCbbl0eTaHuZZsPGo2cA677nrf%22%3A%7B%22tableId%22%3A%22tbl1N9m2h1YAH52P%22%2C%22viewId%22%3A%22vew4RfmKci%22%7D%2C%22W83XbyQuPajhc8s4Ghsc5xF9nTb%22%3A%7B%22tableId%22%3A%22tblXQQeIwMqSFSME%22%2C%22viewId%22%3A%22vewETuuNwn%22%7D%7D; site_env=pre=0; passport_app_access_token=eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3ODc2MzU0NTAsInVuaXQiOiJldV9uYyIsInJhdyI6eyJtX2FjY2Vzc19pbmZvIjp7IjIiOnsiaWF0IjoxNzg3NTkxMzA2LCJhY2Nlc3MiOnRydWV9LCIyOSI6eyJpYXQiOjE3ODc1OTIyNTAsImFjY2VzcyI6dHJ1ZX19LCJzdW0iOiIyM2JkZjExYmFkODA4NjdhYzkzZjM2ZjU4OTc5MmNkZWM3NjBmOTI4Yjg5ZDNiMmFhZGNmYWE0ODRkNzk5MjRjIn19.xuvuwUjW-8fZ-hF6DqjzyjM3AeVb9wLx1dq_kTLCX0jUMMrsW3mHM7xBRjDc9ufTHdM-MZdl5rxnfw9dltLEcg; swp_csrf_token=5bf944ac-c7ef-40db-b815-ef07e0115c98; t_beda37=23bdf7a0fd3e381cc826da49adb45ab435050b508432c3e4ecd7aeb6243780a0; sl_session=eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3ODc2MzY2NjMsInVuaXQiOiJldV9uYyIsInJhdyI6eyJtZXRhIjoiQVdvYzIrdU1BNHZSYUhkQjl4N2lRQUpxTUx6RjlrN012R293dk1YMlRzeThhakM5WXdRQURMSUNLZ0VBUVVGQlFVRkJRVUZCUVVKeGFUVTNVWE5SUkV3eVVUMDkiLCJzdW0iOiIyM2JkZjExYmFkODA4NjdhYzkzZjM2ZjU4OTc5MmNkZWM3NjBmOTI4Yjg5ZDNiMmFhZGNmYWE0ODRkNzk5MjRjIiwibG9jIjoiemhfY24iLCJhcGMiOiJSZWxlYXNlIiwiaWF0IjoxNzg3NTkzNDYzLCJzYWMiOnsiVXNlclN0YWZmU3RhdHVzIjoiMSIsIlVzZXJUeXBlIjoiNDIifSwibG9kIjpudWxsLCJjbmYiOnsiamt0Ijoic3VWaTdnNDNJX0lUb0w2ZGo5QXhEWnBacUhkYldzNWJaaVhvV0s4akZrdyJ9LCJucyI6ImxhcmsiLCJuc191aWQiOiI3NjQ2MjI4MDcyMDgxMDk5NzI5IiwibnNfdGlkIjoiNzUyNzU1NzgzMTgwMzk0NDk2MiIsIm90IjoxLCJjdCI6MTc4NzUzNTA1NywicnQiOjE3ODc1MzUwNTcsInRjcyI6MiwidGJ0IjowfX0.4uYKYGFo9Y-SuLsPofFNvGd7K849RbFdgd62pmoU6npJEWpubrdO7cJyf-yZ8f9-jF5NzFPCm-zJ1RYvYr9jyQ",
- "Connection": "keep-alive"
- }
-
- response = requests.post(url, data=payload, headers=headers, params=querystring)
- result = parse_response(response.text)
-
- # 映射显示名称
- advisor_map = {
- 'optXaXhoOh': '有顾问跟进',
- 'optJdspBAs': '无顾问跟进'
- }
-
- if result.get('error'):
- return {
- '手机号': keyword,
- '批量数据编码': '查询失败',
- '有顾问跟进': result['error'],
- '批量创建时间': '--',
- '批量创建人员': '--'
- }
- else:
- return {
- '手机号': keyword,
- '批量数据编码': result.get('批量数据编码') or '无数据',
- '有顾问跟进': advisor_map.get(result.get('有顾问跟进'), result.get('有顾问跟进') or '未知'),
- '批量创建时间': result.get('批量创建时间') or '无数据',
- '批量创建人员': result.get('批量创建人员') or '无数据'
- }
-
- except Exception as e:
- return {
- '手机号': keyword,
- '批量数据编码': '请求失败',
- '有顾问跟进': str(e),
- '批量创建时间': '--',
- '批量创建人员': '--'
- }
- def batch_search():
- """批量查询"""
- # 获取输入内容
- text_content = text_input.get("1.0", tk.END).strip()
- if not text_content:
- messagebox.showwarning("提示", "请输入要查询的手机号!")
- return
-
- # 解析手机号(支持换行、空格、逗号、分号分隔)
- phones = []
- for sep in ['\n', ',', ',', '、', ';', ';', ' ']:
- if sep in text_content:
- phones = [p.strip() for p in text_content.split(sep) if p.strip()]
- break
- if not phones:
- phones = [text_content.strip()]
-
- # 去重并过滤空值
- phones = list(set([p for p in phones if p]))
-
- if not phones:
- messagebox.showwarning("提示", "未识别到有效的手机号!")
- return
-
- # 确认批量查询
- if len(phones) > 10:
- if not messagebox.askyesno("确认", f"即将查询 {len(phones)} 个手机号,\n批量查询可能需要一些时间,是否继续?"):
- return
-
- # 禁用按钮
- btn_batch_search.config(state='disabled')
- btn_batch_search.config(text='查询中...')
- btn_export.config(state='disabled')
-
- # 清空表格
- tree.delete(*tree.get_children())
-
- # 更新状态
- label_status.config(text=f"正在查询 {len(phones)} 个手机号...", foreground='blue')
- root.update()
-
- # 在新线程中执行查询
- def do_search():
- try:
- results = []
- total = len(phones)
-
- for i, phone in enumerate(phones):
- # 更新状态
- root.after(0, lambda p=phone, idx=i, total=total:
- label_status.config(text=f"正在查询 ({idx+1}/{total}): {p}", foreground='blue'))
- root.update()
-
- # 查询
- result = search_single_phone(phone)
- results.append(result)
-
- # 显示在表格中
- root.after(0, lambda r=result:
- tree.insert('', 'end', values=(
- r['手机号'],
- r['批量数据编码'],
- r['有顾问跟进'],
- r['批量创建时间'],
- r['批量创建人员']
- )))
- root.update()
-
- # 保存结果到内存,供导出使用
- global last_results
- last_results = results
-
- # 更新状态
- root.after(0, lambda: label_status.config(
- text=f"查询完成!共查询 {len(results)} 个手机号,成功 {sum(1 for r in results if r['批量数据编码'] != '查询失败' and r['批量数据编码'] != '请求失败')} 个",
- foreground='green'))
- root.after(0, lambda: btn_export.config(state='normal'))
-
- except Exception as e:
- root.after(0, lambda: messagebox.showerror("错误", f"批量查询失败: {str(e)}"))
- finally:
- root.after(0, lambda: btn_batch_search.config(state='normal'))
- root.after(0, lambda: btn_batch_search.config(text='批量查询'))
-
- Thread(target=do_search, daemon=True).start()
- def export_to_excel():
- """导出结果到Excel"""
- if not tree.get_children():
- messagebox.showwarning("提示", "没有数据可导出,请先执行查询!")
- return
-
- # 获取表格数据
- data = []
- for item in tree.get_children():
- values = tree.item(item)['values']
- data.append({
- '手机号': values[0],
- '批量数据编码': values[1],
- '有顾问跟进': values[2],
- '批量创建时间': values[3],
- '批量创建人员': values[4]
- })
-
- if not data:
- messagebox.showwarning("提示", "没有数据可导出!")
- return
-
- # 选择保存位置
- file_path = filedialog.asksaveasfilename(
- defaultextension=".xlsx",
- filetypes=[("Excel文件", "*.xlsx"), ("CSV文件", "*.csv")],
- title="保存查询结果",
- initialfile=f"查询结果_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
- )
-
- if not file_path:
- return
-
- try:
- df = pd.DataFrame(data)
- df.to_excel(file_path, index=False, engine='openpyxl')
- messagebox.showinfo("成功", f"数据已导出到:\n{file_path}")
- label_status.config(text=f"已导出到:{os.path.basename(file_path)}", foreground='green')
- except Exception as e:
- messagebox.showerror("错误", f"导出失败: {str(e)}")
- def clear_all():
- """清空所有"""
- text_input.delete("1.0", tk.END)
- tree.delete(*tree.get_children())
- label_status.config(text="已清空", foreground='gray')
- btn_export.config(state='disabled')
- # 创建GUI窗口
- root = tk.Tk()
- root.title("飞书多维表格批量查询工具")
- root.geometry("900x650")
- root.resizable(False, False)
- # 设置样式
- style = ttk.Style()
- style.theme_use('clam')
- # 顶部:输入区域
- frame_top = ttk.LabelFrame(root, text="输入手机号(支持批量,用换行/逗号/空格分隔)", padding="10")
- frame_top.pack(fill='x', padx=10, pady=5)
- text_input = tk.Text(frame_top, font=('微软雅黑', 10), height=6, wrap='word')
- text_input.pack(fill='x')
- # 输入提示
- hint_frame = ttk.Frame(frame_top)
- hint_frame.pack(fill='x', pady=(5, 0))
- ttk.Label(hint_frame, text="提示:每行输入一个手机号,或使用逗号、空格分隔", font=('微软雅黑', 9), foreground='gray').pack(side='left')
- # 按钮区域
- btn_frame = ttk.Frame(frame_top)
- btn_frame.pack(fill='x', pady=(5, 0))
- btn_batch_search = ttk.Button(btn_frame, text="批量查询", command=batch_search, width=12)
- btn_batch_search.pack(side='left', padx=(0, 10))
- btn_clear = ttk.Button(btn_frame, text="清空", command=clear_all, width=10)
- btn_clear.pack(side='left', padx=(0, 10))
- btn_export = ttk.Button(btn_frame, text="导出Excel", command=export_to_excel, width=12, state='disabled')
- btn_export.pack(side='left')
- # 中间:结果显示表格
- frame_table = ttk.LabelFrame(root, text="查询结果", padding="10")
- frame_table.pack(fill='both', expand=True, padx=10, pady=5)
- # 创建Treeview(增加手机号列)
- columns = ('手机号', '批量数据编码', '有顾问跟进', '批量创建时间', '批量创建人员')
- tree = ttk.Treeview(frame_table, columns=columns, show='headings', height=12)
- # 设置列标题和宽度
- tree.heading('手机号', text='手机号')
- tree.heading('批量数据编码', text='批量数据编码')
- tree.heading('有顾问跟进', text='有顾问跟进')
- tree.heading('批量创建时间', text='批量创建时间')
- tree.heading('批量创建人员', text='批量创建人员')
- tree.column('手机号', width=150, anchor='center')
- tree.column('批量数据编码', width=180, anchor='center')
- tree.column('有顾问跟进', width=130, anchor='center')
- tree.column('批量创建时间', width=180, anchor='center')
- tree.column('批量创建人员', width=150, anchor='center')
- # 添加滚动条
- scrollbar = ttk.Scrollbar(frame_table, orient='vertical', command=tree.yview)
- tree.configure(yscrollcommand=scrollbar.set)
- tree.pack(side='left', fill='both', expand=True)
- scrollbar.pack(side='right', fill='y')
- # 底部:状态栏
- frame_bottom = ttk.Frame(root, padding="5")
- frame_bottom.pack(fill='x')
- label_status = ttk.Label(frame_bottom, text="请输入手机号,支持批量查询", font=('微软雅黑', 9), foreground='gray')
- label_status.pack(side='left')
- # 存储最近一次查询结果(用于导出)
- last_results = []
- # 运行主循环
- root.mainloop()
|