fengmiguoji commited on
Commit
7dc9195
·
verified ·
1 Parent(s): 1460f27

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +275 -0
app.py ADDED
@@ -0,0 +1,275 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import requests
2
+ import json
3
+ import datetime
4
+ import time # 用于时间转换
5
+ import gradio as gr
6
+
7
+ # --- 抖音配置信息 ---
8
+ DOUYIN_CLIENT_KEY = 'awbeykzyos7kbidv'
9
+ DOUYIN_CLIENT_SECRET = '4575440b156ecbe144284e4f69d284a2' # Keep secure!
10
+ DOUYIN_ACCOUNT_ID = '7241078611527075855'
11
+ DOUYIN_TOKEN_URL = 'https://open.douyin.com/oauth/client_token/'
12
+ DOUYIN_ORDER_QUERY_URL = 'https://open.douyin.com/goodlife/v1/trade/order/query/'
13
+
14
+ # --- 飞书配置信息 ---
15
+ FEISHU_APP_ID = "cli_a6672cae343ad00e"
16
+ FEISHU_APP_SECRET = "0J4SpfBMeIxJEOXDJMNbofMipRgwkMpV" # Keep secure!
17
+ FEISHU_APP_TOKEN = "GcTMbpdDPaAxV5sb8gvcRVC3ndh"
18
+
19
+ # 表格 1 (订单记录)
20
+ FEISHU_TABLE1_ID = "tblaYUUrFT1rKXNB"
21
+ FEISHU_TABLE1_ORDER_FIELD = "订单号" # 文本类型
22
+ FEISHU_TABLE1_STATUS_FIELD = "履约状态" # 文本类型 - 将被更新为 "已兑换"
23
+ TABLE1_EXCHANGED_STATUS_CONTENT = "已兑换" # Table 1 更新后的状态
24
+
25
+ # 表格 2 (激活码/发送记录)
26
+ FEISHU_TABLE2_ID = "tbl0S1kl9FTBX8oT"
27
+ FEISHU_TABLE2_ACTIVATION_CODE_FIELD = "激活码" # Table 2 中激活码字段名 (假设为文本)
28
+ FEISHU_TABLE2_STATUS_FIELD = "发送状态" # Table 2 中发送状态字段名 (文本类型)
29
+ FEISHU_TABLE2_SORT_FIELD = "最后更新时间" # Table 2 中用于排序的字段名
30
+ FEISHU_TABLE2_UPDATE_TIME_FIELD = "最后更新时间" # Table 2 中最后更新日期字段名
31
+ TABLE2_UNSENT_STATUS_CONTENT = "未发送" # Table 2 中查找的状态
32
+ TABLE2_SENT_STATUS_CONTENT = "已发送" # Table 2 更新后的状态
33
+
34
+ # --- 抖音券(item)状态映射字典 ---
35
+ ITEM_STATUS_MAP = {
36
+ 0: "初始化", 1: "交易成功", 10: "待支付", 20: "支付成功",
37
+ 100: "待使用", 101: "交易关闭", 200: "预约中", 201: "已预约",
38
+ 300: "退款中", 301: "已退款", 400: "履约中", 401: "已履约"
39
+ }
40
+
41
+ # =========== 必须包含所有 API 函数定义 ===========
42
+
43
+ def get_douyin_access_token(client_key, client_secret):
44
+ headers = {'Content-Type': 'application/json'}
45
+ payload = {"grant_type": "client_credential", "client_key": client_key, "client_secret": client_secret}
46
+ try:
47
+ response = requests.post(DOUYIN_TOKEN_URL, headers=headers, json=payload, timeout=10); response.raise_for_status(); data = response.json()
48
+ if data.get('data') and data['data'].get('error_code') == 0: return data['data'].get('access_token'), "成功获取抖音 Access Token!"
49
+ else: msg = f"获取抖音 Access Token 失败: Code={data.get('data', {}).get('error_code', 'N/A')}, Desc={data.get('data', {}).get('description', '未知错误')}"; return None, msg
50
+ except Exception as e: msg = f"请求抖音 Access Token 时发生错误: {e}"; return None, msg
51
+
52
+ def query_douyin_order(access_token, account_id, order_id_str):
53
+ if not access_token: return None, "抖音 Token 无效"
54
+ headers = {'Content-Type': 'application/json', 'access-token': access_token}
55
+ params = {'account_id': account_id, 'order_id': order_id_str, 'page_num': 1, 'page_size': 1}
56
+ try:
57
+ response = requests.get(DOUYIN_ORDER_QUERY_URL, headers=headers, params=params, timeout=15); response.raise_for_status(); data = response.json()
58
+ extra_error_code = data.get('extra', {}).get('error_code', 0); data_error_code = data.get('data', {}).get('error_code', 0)
59
+ if extra_error_code == 0 and data_error_code == 0: return data, f"抖音订单 '{order_id_str}' 查询成功!"
60
+ 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
61
+ except Exception as e: msg = f"查询抖音订单时发生错误: {e}"; return None, msg
62
+
63
+ def parse_douyin_order_status(order_data):
64
+ try:
65
+ 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, "抖音订单数据无效。"
66
+ order = order_data['data']['orders'][0]; certificates = order.get('certificate', [])
67
+ if not certificates: return None, None, "订单中未找到券信息。"
68
+ first_cert_status_code = certificates[0].get('item_status'); certificate_id = certificates[0].get('certificate_id', 'N/A')
69
+ 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
70
+ else: return None, None, "第一个券信息中未找到 'item_status'。"
71
+ except Exception as e: msg = f"解析抖音订单状态时出错: {e}"; return None, None, msg
72
+
73
+ def get_feishu_tenant_access_token(app_id, app_secret):
74
+ auth_url = "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal"
75
+ payload = json.dumps({"app_id": app_id, "app_secret": app_secret})
76
+ headers = {'Content-Type': 'application/json; charset=utf-8'}
77
+ try:
78
+ response = requests.post(auth_url, headers=headers, data=payload, timeout=10); response.raise_for_status(); data = response.json()
79
+ if data.get("code") == 0: return data.get("tenant_access_token"), "获取飞书 tenant_access_token 成功!"
80
+ else: msg = f"获取飞书 tenant_access_token 失败: {data.get('msg')} (Code: {data.get('code')})"; return None, msg
81
+ except Exception as e: msg = f"请求飞书 tenant_access_token 时发生错误: {e}"; return None, msg
82
+
83
+ def check_order_exists_in_feishu(access_token, app_token, table_id,
84
+ order_field_name, order_id_str_to_check):
85
+ if not access_token: return None, "飞书 Access Token 无效,无法检查记录。"
86
+ search_url = f"https://open.feishu.cn/open-apis/bitable/v1/apps/{app_token}/tables/{table_id}/records/search"
87
+ headers = {'Authorization': f'Bearer {access_token}', 'Content-Type': 'application/json; charset=utf-8'}
88
+ filter_payload = {"filter": {"conjunction": "and", "conditions": [{"field_name": order_field_name, "operator": "is", "value": [ order_id_str_to_check ]}]}, "page_size": 1}
89
+ msg = f"正在 Table 1 ({table_id}) 中检查订单号 '{order_id_str_to_check}'..."
90
+ try:
91
+ response = requests.post(search_url, headers=headers, json=filter_payload, timeout=15); response.raise_for_status(); data = response.json()
92
+ if data.get("code") == 0:
93
+ items = data.get("data", {}).get("items", [])
94
+ exists = bool(items)
95
+ msg += f" 检查结果: {'已存在' if exists else '不存在'}。"
96
+ return exists, msg # 返回布尔值和消息
97
+ else:
98
+ msg += f" 检查失败: Code={data.get('code')}, Msg={data.get('msg')}"
99
+ return None, msg # 返回 None 表示检查出错
100
+ except Exception as e:
101
+ msg += f" 检查时发生错误: {e}"
102
+ return None, msg
103
+
104
+ def add_bitable_record(access_token, app_token, table_id,
105
+ order_field_name, order_id_str,
106
+ status_field_name, exchange_status):
107
+ if not access_token: return None, "飞书 Access Token 无效,无法写入数据。"
108
+ write_url = f"https://open.feishu.cn/open-apis/bitable/v1/apps/{app_token}/tables/{table_id}/records"
109
+ headers = {'Authorization': f'Bearer {access_token}', 'Content-Type': 'application/json; charset=utf-8'}
110
+ payload = {"fields": {order_field_name: order_id_str, status_field_name: exchange_status}}
111
+ msg = f"准备写入 Table 1 ({table_id})..."
112
+ try:
113
+ response = requests.post(write_url, headers=headers, json=payload, timeout=15); response.raise_for_status(); data = response.json()
114
+ if data.get("code") == 0:
115
+ record_info = data.get('data', {}).get('record')
116
+ record_id = record_info.get('record_id') if record_info else None
117
+ msg += f" Table 1 写入成功!Record ID: {record_id}"
118
+ return record_id, msg # 返回 record_id 和消息
119
+ else:
120
+ msg += f" Table 1 写入失败: Code={data.get('code')}, Msg={data.get('msg')}"
121
+ return None, msg
122
+ except Exception as e:
123
+ msg += f" Table 1 写入时发生错误: {e}"
124
+ return None, msg
125
+
126
+ # --- 修改:查找 Table 2 记录,并只提取激活码文本 ---
127
+ def find_first_unsent_record_and_code_text(access_token, app_token, table_id,
128
+ status_field, unsent_status_value,
129
+ activation_code_field, sort_field):
130
+ """
131
+ 在 Table 2 查找第一个状态为 '未发送' 的记录,返回其 record_id 和激活码文本。
132
+ """
133
+ if not access_token: print("内部错误: 飞书 Token 无效"); return None, None
134
+ search_url = f"https://open.feishu.cn/open-apis/bitable/v1/apps/{app_token}/tables/{table_id}/records/search"
135
+ headers = {'Authorization': f'Bearer {access_token}', 'Content-Type': 'application/json; charset=utf-8'}
136
+ payload = {
137
+ "filter": {"conjunction": "and", "conditions": [{"field_name": status_field, "operator": "is", "value": [unsent_status_value]}]},
138
+ "sort": [{"field_name": sort_field, "order": "asc"}],
139
+ "page_size": 1,
140
+ # 【修改这里】: 直接传递 Python 列表,不要 json.dumps()
141
+ "field_names": [activation_code_field]
142
+ }
143
+ print(f"飞书API: 查找 Table 2 '{status_field}' 为 '{unsent_status_value}' 的记录...") # 后台日志
144
+ # print("构造的查找 Payload:", json.dumps(payload, indent=2, ensure_ascii=False)) # 调试时可以打印看看
145
+ try:
146
+ response = requests.post(search_url, headers=headers, json=payload, timeout=15); response.raise_for_status(); data = response.json()
147
+ if data.get("code") == 0:
148
+ items = data.get("data", {}).get("items", [])
149
+ if items:
150
+ # ... (后续解析激活码的逻辑不变) ...
151
+ record = items[0]; record_id = record.get('record_id')
152
+ activation_code_raw = record.get('fields', {}).get(activation_code_field)
153
+ activation_code_text = None
154
+ 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]:
155
+ activation_code_text = activation_code_raw[0]['text']
156
+ elif isinstance(activation_code_raw, str):
157
+ activation_code_text = activation_code_raw
158
+ elif activation_code_raw is not None:
159
+ try: activation_code_text = str(activation_code_raw); print(f"警告: 激活码字段值类型为 {type(activation_code_raw)}, 已尝试转为字符串。")
160
+ except: print(f"警告: 无法将激活码字段值 {activation_code_raw} 转为字符串。")
161
+ else: print(f"警告: 激活码字段值为空或格式未知: {activation_code_raw}")
162
+
163
+ if record_id and activation_code_text is not None:
164
+ print(f"飞书API: 找到记录 ID: {record_id}, 激活码文本: {activation_code_text}"); return record_id, activation_code_text
165
+ elif record_id: print(f"飞书API: 找到记录 ID: {record_id}, 但未能提取激活码文本。"); return record_id, None
166
+ else: return None, None
167
+ else:
168
+ print(f"飞书API: 未找到 '{status_field}' 为 '{unsent_status_value}' 的记录。"); return None, None
169
+ else:
170
+ print(f"飞书API: 查找 Table 2 记录失败: Code={data.get('code')}, Msg={data.get('msg')}"); return None, None
171
+ except Exception as e:
172
+ print(f"飞书API: 查找 Table 2 记录时发生错误: {e}"); return None, None
173
+
174
+ def update_table2_record_to_sent(access_token, app_token, table_id, record_id,
175
+ status_field, sent_status_content,
176
+ update_time_field):
177
+ if not access_token or not record_id: return False, "飞书 Token 或 Table 2 Record ID 无效,无法更新。"
178
+ update_url = f"https://open.feishu.cn/open-apis/bitable/v1/apps/{app_token}/tables/{table_id}/records/batch_update"
179
+ headers = {'Authorization': f'Bearer {access_token}', 'Content-Type': 'application/json; charset=utf-8'}
180
+ now = datetime.datetime.now(); formatted_time_str = now.strftime("%Y/%m/%d %H:%M")
181
+ payload_fields = {status_field: sent_status_content, update_time_field: formatted_time_str}
182
+ payload = {"records": [{"record_id": record_id, "fields": payload_fields}]}
183
+ msg = f"准备更新 Table 2 ({table_id}, ID: {record_id}) 为 '{sent_status_content}'..."
184
+ try:
185
+ response = requests.post(update_url, headers=headers, json=payload, timeout=15); response.raise_for_status(); data = response.json()
186
+ if data.get("code") == 0:
187
+ failed_records = data.get("data", {}).get("failed_records", [])
188
+ if not failed_records: msg += " Table 2 更新成功!"; return True, msg
189
+ else: msg += f" Table 2 更新失败: {failed_records}"; return False, msg
190
+ else: msg += f" Table 2 更新失败: Code={data.get('code')}, Msg={data.get('msg')}"; return False, msg
191
+ except Exception as e: msg += f" Table 2 更新时发生错误: {e}"; return False, msg
192
+
193
+ def update_table1_record_to_exchanged(access_token, app_token, table_id, record_id,
194
+ status_field, exchanged_status_content):
195
+ if not access_token or not record_id: return False, "飞书 Token 或 Table 1 Record ID 无效,无法更新。"
196
+ update_url = f"https://open.feishu.cn/open-apis/bitable/v1/apps/{app_token}/tables/{table_id}/records/batch_update"
197
+ headers = {'Authorization': f'Bearer {access_token}', 'Content-Type': 'application/json; charset=utf-8'}
198
+ payload = {"records": [{"record_id": record_id, "fields": {status_field: exchanged_status_content}}]}
199
+ msg = f"准备更新 Table 1 ({table_id}, ID: {record_id}) 为 '{exchanged_status_content}'..."
200
+ try:
201
+ response = requests.post(update_url, headers=headers, json=payload, timeout=15); response.raise_for_status(); data = response.json()
202
+ if data.get("code") == 0:
203
+ failed_records = data.get("data", {}).get("failed_records", [])
204
+ if not failed_records: msg += " Table 1 更新成功!"; return True, msg
205
+ else: msg += f" Table 1 更新失败: {failed_records}"; return False, msg
206
+ else: msg += f" Table 1 更新失败: Code={data.get('code')}, Msg={data.get('msg')}"; return False, msg
207
+ except Exception as e: msg += f" Table 1 更新时发生错误: {e}"; return False, msg
208
+
209
+
210
+ # --- Gradio 主处理函数 (简化版) ---
211
+ def process_order_simple(douyin_order_id_str):
212
+ final_output = "处理中..."
213
+ # --- 1. 获取抖音 Token ---
214
+ douyin_token, msg = get_douyin_access_token(DOUYIN_CLIENT_KEY, DOUYIN_CLIENT_SECRET)
215
+ if not douyin_token: return f"错误(抖音Token): {msg}"
216
+ # --- 2. 验证输入 ---
217
+ douyin_order_id_str = douyin_order_id_str.strip()
218
+ if not douyin_order_id_str: return "请输入抖音订单号"
219
+ # --- 3. 查询抖音订单 ---
220
+ order_details, msg = query_douyin_order(douyin_token, DOUYIN_ACCOUNT_ID, douyin_order_id_str)
221
+ if not order_details: return f"错误(查询订单): {msg}"
222
+ # --- 4. 解析抖音状态 ---
223
+ fulfillment_status_text, _, msg = parse_douyin_order_status(order_details)
224
+ if fulfillment_status_text is None: return f"错误(解析状态): {msg}"
225
+ # --- 5. 检查抖音状态 ---
226
+ if fulfillment_status_text != "已履约": return f"提示:订单状态为 '{fulfillment_status_text}',不予兑换。"
227
+ # --- 6. 获取飞书 Token ---
228
+ feishu_token, msg = get_feishu_tenant_access_token(FEISHU_APP_ID, FEISHU_APP_SECRET)
229
+ if not feishu_token: return f"错误(飞书Token): {msg}"
230
+ # --- 7. 检查 Table 1 ---
231
+ 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)
232
+ if exists_in_table1 is None: return f"错误(检查Table1): {msg}"
233
+ if exists_in_table1 is True: return f"提示:订单 '{douyin_order_id_str}' 已兑换过。"
234
+ # --- 8. 写入 Table 1 ---
235
+ 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)
236
+ if not table1_record_id: return f"错误(写入Table1): {msg}"
237
+ print(f"后台日志: Table 1 写入成功, Record ID: {table1_record_id}")
238
+ # --- 9. 查找 Table 2 ---
239
+ 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)
240
+ if table2_record_id and activation_code_text:
241
+ final_output = f"{activation_code_text}" # 直接显示激活码
242
+ # --- 10. "发送" 激活码 (模拟) ---
243
+ print(f"后台日志: '发送' 激活码: {activation_code_text}")
244
+ sent_success = True # 假设发送成功
245
+ if sent_success:
246
+ # --- 11. 更新 Table 2 ---
247
+ 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)
248
+ if not update_t2_success: print(f"后台警告: {msg_t2}") # 只在后台打印警告
249
+ if update_t2_success: # 只有 T2 更新成功才更新 T1
250
+ # --- 12. 更新 Table 1 ---
251
+ 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)
252
+ if not update_t1_success: print(f"后台警告: {msg_t1}") # 只在后台打印警告
253
+ else: final_output = "错误:激活码 '发送' 失败 (模拟)"
254
+ elif table2_record_id and not activation_code_text: final_output = f"错误:未能获取激活码文本,请检查 Table 2 数据。"
255
+ else: final_output = "提示:无可用激活码,请联系管理员。"
256
+ return final_output # 返回最终结果
257
+
258
+ # --- 创建 Gradio Interface (最终简化版) ---
259
+ with gr.Blocks(theme=gr.themes.Soft()) as demo:
260
+ gr.Markdown("# 竞潮玩游戏激活码兑换小助手")
261
+ gr.Markdown("请输入抖音订单号以兑换激活码。仅限状态为“已履约”且未兑换过的订单。")
262
+ with gr.Row():
263
+ order_id_input = gr.Textbox(label="输入抖音订单号", placeholder="在此填写订单号...")
264
+ process_button = gr.Button("兑换激活码")
265
+ with gr.Row():
266
+ activation_code_output = gr.Textbox(label="兑换结果 / 激活码", interactive=False)
267
+ process_button.click(
268
+ fn=process_order_simple,
269
+ inputs=order_id_input,
270
+ outputs=activation_code_output
271
+ )
272
+
273
+ # --- 启动 Gradio 应用 ---
274
+ if __name__ == "__main__":
275
+ demo.launch()