gamekey / app.py
fengmiguoji's picture
Upload app.py
7dc9195 verified
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()