Spaces:
Sleeping
Sleeping
| import requests | |
| import json | |
| import datetime | |
| import time # 用于时间转换 | |
| import gradio as gr | |
| # --- 抖音配置信息 --- | |
| DOUYIN_CLIENT_KEY = 'awbeykzyos7kbidv' | |
| DOUYIN_CLIENT_SECRET = '4575440b156ecbe144284e4f69d284a2' # Keep secure! | |
| DOUYIN_ACCOUNT_ID = '7241078611527075855' | |
| DOUYIN_TOKEN_URL = 'https://open.douyin.com/oauth/client_token/' | |
| DOUYIN_ORDER_QUERY_URL = 'https://open.douyin.com/goodlife/v1/trade/order/query/' | |
| # --- 飞书配置信息 --- | |
| FEISHU_APP_ID = "cli_a6672cae343ad00e" | |
| FEISHU_APP_SECRET = "0J4SpfBMeIxJEOXDJMNbofMipRgwkMpV" # Keep secure! | |
| FEISHU_APP_TOKEN = "GcTMbpdDPaAxV5sb8gvcRVC3ndh" | |
| # 表格 1 (订单记录) | |
| FEISHU_TABLE1_ID = "tblaYUUrFT1rKXNB" | |
| FEISHU_TABLE1_ORDER_FIELD = "订单号" # 文本类型 | |
| FEISHU_TABLE1_STATUS_FIELD = "履约状态" # 文本类型 - 将被更新为 "已兑换" | |
| TABLE1_EXCHANGED_STATUS_CONTENT = "已兑换" # Table 1 更新后的状态 | |
| # 表格 2 (激活码/发送记录) | |
| FEISHU_TABLE2_ID = "tbl0S1kl9FTBX8oT" | |
| FEISHU_TABLE2_ACTIVATION_CODE_FIELD = "激活码" # Table 2 中激活码字段名 (假设为文本) | |
| FEISHU_TABLE2_STATUS_FIELD = "发送状态" # Table 2 中发送状态字段名 (文本类型) | |
| FEISHU_TABLE2_SORT_FIELD = "最后更新时间" # Table 2 中用于排序的字段名 | |
| FEISHU_TABLE2_UPDATE_TIME_FIELD = "最后更新时间" # Table 2 中最后更新日期字段名 | |
| TABLE2_UNSENT_STATUS_CONTENT = "未发送" # Table 2 中查找的状态 | |
| TABLE2_SENT_STATUS_CONTENT = "已发送" # Table 2 更新后的状态 | |
| # --- 抖音券(item)状态映射字典 --- | |
| ITEM_STATUS_MAP = { | |
| 0: "初始化", 1: "交易成功", 10: "待支付", 20: "支付成功", | |
| 100: "待使用", 101: "交易关闭", 200: "预约中", 201: "已预约", | |
| 300: "退款中", 301: "已退款", 400: "履约中", 401: "已履约" | |
| } | |
| # =========== 必须包含所有 API 函数定义 =========== | |
| def get_douyin_access_token(client_key, client_secret): | |
| headers = {'Content-Type': 'application/json'} | |
| payload = {"grant_type": "client_credential", "client_key": client_key, "client_secret": client_secret} | |
| try: | |
| response = requests.post(DOUYIN_TOKEN_URL, headers=headers, json=payload, timeout=10); response.raise_for_status(); data = response.json() | |
| if data.get('data') and data['data'].get('error_code') == 0: return data['data'].get('access_token'), "成功获取抖音 Access Token!" | |
| else: msg = f"获取抖音 Access Token 失败: Code={data.get('data', {}).get('error_code', 'N/A')}, Desc={data.get('data', {}).get('description', '未知错误')}"; return None, msg | |
| except Exception as e: msg = f"请求抖音 Access Token 时发生错误: {e}"; return None, msg | |
| def query_douyin_order(access_token, account_id, order_id_str): | |
| if not access_token: return None, "抖音 Token 无效" | |
| headers = {'Content-Type': 'application/json', 'access-token': access_token} | |
| params = {'account_id': account_id, 'order_id': order_id_str, 'page_num': 1, 'page_size': 1} | |
| try: | |
| response = requests.get(DOUYIN_ORDER_QUERY_URL, headers=headers, params=params, timeout=15); response.raise_for_status(); data = response.json() | |
| extra_error_code = data.get('extra', {}).get('error_code', 0); data_error_code = data.get('data', {}).get('error_code', 0) | |
| if extra_error_code == 0 and data_error_code == 0: return data, f"抖音订单 '{order_id_str}' 查询成功!" | |
| else: error_code = extra_error_code if extra_error_code != 0 else data_error_code; description = data.get('extra', {}).get('description', '') or data.get('data', {}).get('description', '未知错误'); msg = f"抖音订单查询失败: Code={error_code}, Desc={description}"; return None, msg | |
| except Exception as e: msg = f"查询抖音订单时发生错误: {e}"; return None, msg | |
| def parse_douyin_order_status(order_data): | |
| try: | |
| if not order_data or 'data' not in order_data or 'orders' not in order_data['data'] or not order_data['data']['orders']: return None, None, "抖音订单数据无效。" | |
| order = order_data['data']['orders'][0]; certificates = order.get('certificate', []) | |
| if not certificates: return None, None, "订单中未找到券信息。" | |
| first_cert_status_code = certificates[0].get('item_status'); certificate_id = certificates[0].get('certificate_id', 'N/A') | |
| if first_cert_status_code is not None: status_text = ITEM_STATUS_MAP.get(first_cert_status_code, f"未知状态码({first_cert_status_code})"); msg=f"解析到抖音订单状态: {status_text} (状态码: {first_cert_status_code}, 券ID: {certificate_id})"; return status_text, first_cert_status_code, msg | |
| else: return None, None, "第一个券信息中未找到 'item_status'。" | |
| except Exception as e: msg = f"解析抖音订单状态时出错: {e}"; return None, None, msg | |
| def get_feishu_tenant_access_token(app_id, app_secret): | |
| auth_url = "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal" | |
| payload = json.dumps({"app_id": app_id, "app_secret": app_secret}) | |
| headers = {'Content-Type': 'application/json; charset=utf-8'} | |
| try: | |
| response = requests.post(auth_url, headers=headers, data=payload, timeout=10); response.raise_for_status(); data = response.json() | |
| if data.get("code") == 0: return data.get("tenant_access_token"), "获取飞书 tenant_access_token 成功!" | |
| else: msg = f"获取飞书 tenant_access_token 失败: {data.get('msg')} (Code: {data.get('code')})"; return None, msg | |
| except Exception as e: msg = f"请求飞书 tenant_access_token 时发生错误: {e}"; return None, msg | |
| def check_order_exists_in_feishu(access_token, app_token, table_id, | |
| order_field_name, order_id_str_to_check): | |
| if not access_token: return None, "飞书 Access Token 无效,无法检查记录。" | |
| search_url = f"https://open.feishu.cn/open-apis/bitable/v1/apps/{app_token}/tables/{table_id}/records/search" | |
| headers = {'Authorization': f'Bearer {access_token}', 'Content-Type': 'application/json; charset=utf-8'} | |
| filter_payload = {"filter": {"conjunction": "and", "conditions": [{"field_name": order_field_name, "operator": "is", "value": [ order_id_str_to_check ]}]}, "page_size": 1} | |
| msg = f"正在 Table 1 ({table_id}) 中检查订单号 '{order_id_str_to_check}'..." | |
| try: | |
| response = requests.post(search_url, headers=headers, json=filter_payload, timeout=15); response.raise_for_status(); data = response.json() | |
| if data.get("code") == 0: | |
| items = data.get("data", {}).get("items", []) | |
| exists = bool(items) | |
| msg += f" 检查结果: {'已存在' if exists else '不存在'}。" | |
| return exists, msg # 返回布尔值和消息 | |
| else: | |
| msg += f" 检查失败: Code={data.get('code')}, Msg={data.get('msg')}" | |
| return None, msg # 返回 None 表示检查出错 | |
| except Exception as e: | |
| msg += f" 检查时发生错误: {e}" | |
| return None, msg | |
| def add_bitable_record(access_token, app_token, table_id, | |
| order_field_name, order_id_str, | |
| status_field_name, exchange_status): | |
| if not access_token: return None, "飞书 Access Token 无效,无法写入数据。" | |
| write_url = f"https://open.feishu.cn/open-apis/bitable/v1/apps/{app_token}/tables/{table_id}/records" | |
| headers = {'Authorization': f'Bearer {access_token}', 'Content-Type': 'application/json; charset=utf-8'} | |
| payload = {"fields": {order_field_name: order_id_str, status_field_name: exchange_status}} | |
| msg = f"准备写入 Table 1 ({table_id})..." | |
| try: | |
| response = requests.post(write_url, headers=headers, json=payload, timeout=15); response.raise_for_status(); data = response.json() | |
| if data.get("code") == 0: | |
| record_info = data.get('data', {}).get('record') | |
| record_id = record_info.get('record_id') if record_info else None | |
| msg += f" Table 1 写入成功!Record ID: {record_id}" | |
| return record_id, msg # 返回 record_id 和消息 | |
| else: | |
| msg += f" Table 1 写入失败: Code={data.get('code')}, Msg={data.get('msg')}" | |
| return None, msg | |
| except Exception as e: | |
| msg += f" Table 1 写入时发生错误: {e}" | |
| return None, msg | |
| # --- 修改:查找 Table 2 记录,并只提取激活码文本 --- | |
| def find_first_unsent_record_and_code_text(access_token, app_token, table_id, | |
| status_field, unsent_status_value, | |
| activation_code_field, sort_field): | |
| """ | |
| 在 Table 2 查找第一个状态为 '未发送' 的记录,返回其 record_id 和激活码文本。 | |
| """ | |
| if not access_token: print("内部错误: 飞书 Token 无效"); return None, None | |
| search_url = f"https://open.feishu.cn/open-apis/bitable/v1/apps/{app_token}/tables/{table_id}/records/search" | |
| headers = {'Authorization': f'Bearer {access_token}', 'Content-Type': 'application/json; charset=utf-8'} | |
| payload = { | |
| "filter": {"conjunction": "and", "conditions": [{"field_name": status_field, "operator": "is", "value": [unsent_status_value]}]}, | |
| "sort": [{"field_name": sort_field, "order": "asc"}], | |
| "page_size": 1, | |
| # 【修改这里】: 直接传递 Python 列表,不要 json.dumps() | |
| "field_names": [activation_code_field] | |
| } | |
| print(f"飞书API: 查找 Table 2 '{status_field}' 为 '{unsent_status_value}' 的记录...") # 后台日志 | |
| # print("构造的查找 Payload:", json.dumps(payload, indent=2, ensure_ascii=False)) # 调试时可以打印看看 | |
| try: | |
| response = requests.post(search_url, headers=headers, json=payload, timeout=15); response.raise_for_status(); data = response.json() | |
| if data.get("code") == 0: | |
| items = data.get("data", {}).get("items", []) | |
| if items: | |
| # ... (后续解析激活码的逻辑不变) ... | |
| record = items[0]; record_id = record.get('record_id') | |
| activation_code_raw = record.get('fields', {}).get(activation_code_field) | |
| activation_code_text = None | |
| if isinstance(activation_code_raw, list) and len(activation_code_raw) > 0 and isinstance(activation_code_raw[0], dict) and 'text' in activation_code_raw[0]: | |
| activation_code_text = activation_code_raw[0]['text'] | |
| elif isinstance(activation_code_raw, str): | |
| activation_code_text = activation_code_raw | |
| elif activation_code_raw is not None: | |
| try: activation_code_text = str(activation_code_raw); print(f"警告: 激活码字段值类型为 {type(activation_code_raw)}, 已尝试转为字符串。") | |
| except: print(f"警告: 无法将激活码字段值 {activation_code_raw} 转为字符串。") | |
| else: print(f"警告: 激活码字段值为空或格式未知: {activation_code_raw}") | |
| if record_id and activation_code_text is not None: | |
| print(f"飞书API: 找到记录 ID: {record_id}, 激活码文本: {activation_code_text}"); return record_id, activation_code_text | |
| elif record_id: print(f"飞书API: 找到记录 ID: {record_id}, 但未能提取激活码文本。"); return record_id, None | |
| else: return None, None | |
| else: | |
| print(f"飞书API: 未找到 '{status_field}' 为 '{unsent_status_value}' 的记录。"); return None, None | |
| else: | |
| print(f"飞书API: 查找 Table 2 记录失败: Code={data.get('code')}, Msg={data.get('msg')}"); return None, None | |
| except Exception as e: | |
| print(f"飞书API: 查找 Table 2 记录时发生错误: {e}"); return None, None | |
| def update_table2_record_to_sent(access_token, app_token, table_id, record_id, | |
| status_field, sent_status_content, | |
| update_time_field): | |
| if not access_token or not record_id: return False, "飞书 Token 或 Table 2 Record ID 无效,无法更新。" | |
| update_url = f"https://open.feishu.cn/open-apis/bitable/v1/apps/{app_token}/tables/{table_id}/records/batch_update" | |
| headers = {'Authorization': f'Bearer {access_token}', 'Content-Type': 'application/json; charset=utf-8'} | |
| now = datetime.datetime.now(); formatted_time_str = now.strftime("%Y/%m/%d %H:%M") | |
| payload_fields = {status_field: sent_status_content, update_time_field: formatted_time_str} | |
| payload = {"records": [{"record_id": record_id, "fields": payload_fields}]} | |
| msg = f"准备更新 Table 2 ({table_id}, ID: {record_id}) 为 '{sent_status_content}'..." | |
| try: | |
| response = requests.post(update_url, headers=headers, json=payload, timeout=15); response.raise_for_status(); data = response.json() | |
| if data.get("code") == 0: | |
| failed_records = data.get("data", {}).get("failed_records", []) | |
| if not failed_records: msg += " Table 2 更新成功!"; return True, msg | |
| else: msg += f" Table 2 更新失败: {failed_records}"; return False, msg | |
| else: msg += f" Table 2 更新失败: Code={data.get('code')}, Msg={data.get('msg')}"; return False, msg | |
| except Exception as e: msg += f" Table 2 更新时发生错误: {e}"; return False, msg | |
| def update_table1_record_to_exchanged(access_token, app_token, table_id, record_id, | |
| status_field, exchanged_status_content): | |
| if not access_token or not record_id: return False, "飞书 Token 或 Table 1 Record ID 无效,无法更新。" | |
| update_url = f"https://open.feishu.cn/open-apis/bitable/v1/apps/{app_token}/tables/{table_id}/records/batch_update" | |
| headers = {'Authorization': f'Bearer {access_token}', 'Content-Type': 'application/json; charset=utf-8'} | |
| payload = {"records": [{"record_id": record_id, "fields": {status_field: exchanged_status_content}}]} | |
| msg = f"准备更新 Table 1 ({table_id}, ID: {record_id}) 为 '{exchanged_status_content}'..." | |
| try: | |
| response = requests.post(update_url, headers=headers, json=payload, timeout=15); response.raise_for_status(); data = response.json() | |
| if data.get("code") == 0: | |
| failed_records = data.get("data", {}).get("failed_records", []) | |
| if not failed_records: msg += " Table 1 更新成功!"; return True, msg | |
| else: msg += f" Table 1 更新失败: {failed_records}"; return False, msg | |
| else: msg += f" Table 1 更新失败: Code={data.get('code')}, Msg={data.get('msg')}"; return False, msg | |
| except Exception as e: msg += f" Table 1 更新时发生错误: {e}"; return False, msg | |
| # --- Gradio 主处理函数 (简化版) --- | |
| def process_order_simple(douyin_order_id_str): | |
| final_output = "处理中..." | |
| # --- 1. 获取抖音 Token --- | |
| douyin_token, msg = get_douyin_access_token(DOUYIN_CLIENT_KEY, DOUYIN_CLIENT_SECRET) | |
| if not douyin_token: return f"错误(抖音Token): {msg}" | |
| # --- 2. 验证输入 --- | |
| douyin_order_id_str = douyin_order_id_str.strip() | |
| if not douyin_order_id_str: return "请输入抖音订单号" | |
| # --- 3. 查询抖音订单 --- | |
| order_details, msg = query_douyin_order(douyin_token, DOUYIN_ACCOUNT_ID, douyin_order_id_str) | |
| if not order_details: return f"错误(查询订单): {msg}" | |
| # --- 4. 解析抖音状态 --- | |
| fulfillment_status_text, _, msg = parse_douyin_order_status(order_details) | |
| if fulfillment_status_text is None: return f"错误(解析状态): {msg}" | |
| # --- 5. 检查抖音状态 --- | |
| if fulfillment_status_text != "已履约": return f"提示:订单状态为 '{fulfillment_status_text}',不予兑换。" | |
| # --- 6. 获取飞书 Token --- | |
| feishu_token, msg = get_feishu_tenant_access_token(FEISHU_APP_ID, FEISHU_APP_SECRET) | |
| if not feishu_token: return f"错误(飞书Token): {msg}" | |
| # --- 7. 检查 Table 1 --- | |
| exists_in_table1, msg = check_order_exists_in_feishu(feishu_token, FEISHU_APP_TOKEN, FEISHU_TABLE1_ID, FEISHU_TABLE1_ORDER_FIELD, douyin_order_id_str) | |
| if exists_in_table1 is None: return f"错误(检查Table1): {msg}" | |
| if exists_in_table1 is True: return f"提示:订单 '{douyin_order_id_str}' 已兑换过。" | |
| # --- 8. 写入 Table 1 --- | |
| table1_record_id, msg = add_bitable_record(feishu_token, FEISHU_APP_TOKEN, FEISHU_TABLE1_ID, FEISHU_TABLE1_ORDER_FIELD, douyin_order_id_str, FEISHU_TABLE1_STATUS_FIELD, fulfillment_status_text) | |
| if not table1_record_id: return f"错误(写入Table1): {msg}" | |
| print(f"后台日志: Table 1 写入成功, Record ID: {table1_record_id}") | |
| # --- 9. 查找 Table 2 --- | |
| table2_record_id, activation_code_text = find_first_unsent_record_and_code_text(feishu_token, FEISHU_APP_TOKEN, FEISHU_TABLE2_ID, FEISHU_TABLE2_STATUS_FIELD, TABLE2_UNSENT_STATUS_CONTENT, FEISHU_TABLE2_ACTIVATION_CODE_FIELD, FEISHU_TABLE2_SORT_FIELD) | |
| if table2_record_id and activation_code_text: | |
| final_output = f"{activation_code_text}" # 直接显示激活码 | |
| # --- 10. "发送" 激活码 (模拟) --- | |
| print(f"后台日志: '发送' 激活码: {activation_code_text}") | |
| sent_success = True # 假设发送成功 | |
| if sent_success: | |
| # --- 11. 更新 Table 2 --- | |
| update_t2_success, msg_t2 = update_table2_record_to_sent(feishu_token, FEISHU_APP_TOKEN, FEISHU_TABLE2_ID, table2_record_id, FEISHU_TABLE2_STATUS_FIELD, TABLE2_SENT_STATUS_CONTENT, FEISHU_TABLE2_UPDATE_TIME_FIELD) | |
| if not update_t2_success: print(f"后台警告: {msg_t2}") # 只在后台打印警告 | |
| if update_t2_success: # 只有 T2 更新成功才更新 T1 | |
| # --- 12. 更新 Table 1 --- | |
| update_t1_success, msg_t1 = update_table1_record_to_exchanged(feishu_token, FEISHU_APP_TOKEN, FEISHU_TABLE1_ID, table1_record_id, FEISHU_TABLE1_STATUS_FIELD, TABLE1_EXCHANGED_STATUS_CONTENT) | |
| if not update_t1_success: print(f"后台警告: {msg_t1}") # 只在后台打印警告 | |
| else: final_output = "错误:激活码 '发送' 失败 (模拟)" | |
| elif table2_record_id and not activation_code_text: final_output = f"错误:未能获取激活码文本,请检查 Table 2 数据。" | |
| else: final_output = "提示:无可用激活码,请联系管理员。" | |
| return final_output # 返回最终结果 | |
| # --- 创建 Gradio Interface (最终简化版) --- | |
| with gr.Blocks(theme=gr.themes.Soft()) as demo: | |
| gr.Markdown("# 竞潮玩游戏激活码兑换小助手") | |
| gr.Markdown("请输入抖音订单号以兑换激活码。仅限状态为“已履约”且未兑换过的订单。") | |
| with gr.Row(): | |
| order_id_input = gr.Textbox(label="输入抖音订单号", placeholder="在此填写订单号...") | |
| process_button = gr.Button("兑换激活码") | |
| with gr.Row(): | |
| activation_code_output = gr.Textbox(label="兑换结果 / 激活码", interactive=False) | |
| process_button.click( | |
| fn=process_order_simple, | |
| inputs=order_id_input, | |
| outputs=activation_code_output | |
| ) | |
| # --- 启动 Gradio 应用 --- | |
| if __name__ == "__main__": | |
| demo.launch() | |