yangtb24 commited on
Commit
0ea2dd9
·
verified ·
1 Parent(s): ce156e0

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +478 -476
app.py CHANGED
@@ -1,476 +1,478 @@
1
- import os
2
- import json
3
- import time
4
- import threading
5
- from datetime import datetime
6
- from flask import Flask, request, jsonify, render_template_string, redirect, url_for, session
7
- import requests
8
- from apscheduler.schedulers.background import BackgroundScheduler
9
- from dotenv import load_dotenv
10
-
11
- load_dotenv()
12
-
13
- app = Flask(__name__)
14
- app.secret_key = os.urandom(24)
15
-
16
- LOGIN_URL = "https://api-card.infini.money/user/login"
17
- PROFILE_URL = "https://api-card.infini.money/user/profile"
18
- CARD_INFO_URL = "https://api-card.infini.money/card/info"
19
- FRONTEND_PASSWORD = os.getenv("PASSWORD")
20
- ACCOUNTS_JSON = os.getenv("ACCOUNTS")
21
-
22
- accounts_data = {}
23
- scheduler = BackgroundScheduler(daemon=True)
24
- data_lock = threading.Lock()
25
-
26
- def parse_accounts():
27
- global accounts_data
28
- if not ACCOUNTS_JSON:
29
- print("错误: ACCOUNTS 环境变量未设置。")
30
- return False
31
- try:
32
- accounts_list = json.loads(ACCOUNTS_JSON)
33
- if not isinstance(accounts_list, list):
34
- print("错误: ACCOUNTS 环境变量必须是一个 JSON 数组。")
35
- return False
36
-
37
- temp_accounts_data = {}
38
- for acc in accounts_list:
39
- if isinstance(acc, dict) and "email" in acc and "password" in acc:
40
- temp_accounts_data[acc["email"]] = {
41
- "password": acc["password"],
42
- "token": None,
43
- "profile": None,
44
- "last_login_success": None,
45
- "last_profile_success": None,
46
- "last_login_attempt": None,
47
- "last_profile_attempt": None,
48
- "login_error": None,
49
- "profile_error": None,
50
- "cards_info": None,
51
- "last_card_info_success": None,
52
- "last_card_info_attempt": None,
53
- "card_info_error": None,
54
- }
55
- else:
56
- print(f"警告: ACCOUNTS 中的条目格式不正确: {acc}")
57
-
58
- with data_lock:
59
- accounts_data = temp_accounts_data
60
- print(f"成功加载 {len(accounts_data)} 个账户。")
61
- return True
62
- except json.JSONDecodeError:
63
- print("错误: ACCOUNTS 环境变量不是有效的 JSON 格式。")
64
- return False
65
-
66
- def api_login(email, password):
67
- payload = {"email": email, "password": password}
68
- try:
69
- response = requests.post(LOGIN_URL, json=payload, timeout=10)
70
- response.raise_for_status()
71
- data = response.json()
72
- cookies = response.cookies
73
- jwt_token = cookies.get("jwt_token")
74
-
75
- if data.get("code") == 0 and jwt_token:
76
- return jwt_token, None
77
- else:
78
- error_message = data.get('message', "未知登录错误")
79
- if data.get("code") == 0 and not jwt_token:
80
- error_message = "响应成功但未返回 jwt_token。"
81
- return None, f"{error_message} (Code: {data.get('code')})"
82
- except requests.exceptions.Timeout:
83
- return None, "登录请求超时。"
84
- except requests.exceptions.RequestException as e:
85
- return None, f"登录请求错误: {str(e)}"
86
- except json.JSONDecodeError:
87
- return None, "登录响应不是有效的 JSON。"
88
-
89
- def get_api_profile(email, token):
90
- if not token:
91
- return None, "Token 为空,无法获取 Profile。"
92
-
93
- cookies = {"jwt_token": token}
94
- try:
95
- response = requests.get(PROFILE_URL, cookies=cookies, timeout=10)
96
- response.raise_for_status()
97
- profile_data = response.json()
98
- if profile_data.get("code") == 0 and profile_data.get("data"):
99
- return profile_data.get("data"), None
100
- else:
101
- error_message = profile_data.get('message', "未知 Profile 错误")
102
- if profile_data.get("code") == 0 and not profile_data.get("data"):
103
- error_message = "响应成功但未返回 Profile 数据。"
104
- return None, f"{error_message} (Code: {profile_data.get('code')})"
105
- except requests.exceptions.Timeout:
106
- return None, "获取 Profile 请求超时。"
107
- except requests.exceptions.RequestException as e:
108
- return None, f"获取 Profile 请求错误: {str(e)}"
109
- except json.JSONDecodeError:
110
- return None, "Profile 响应不是有效的 JSON。"
111
-
112
- def get_api_card_info(email, token):
113
- if not token:
114
- return None, "Token 为空,无法获取卡片信息。"
115
-
116
- cookies = {"jwt_token": token}
117
- print(f"[{datetime.now()}] 尝试为账户 {email} 获取卡片信息...")
118
- try:
119
- response = requests.get(CARD_INFO_URL, cookies=cookies, timeout=10)
120
- response.raise_for_status()
121
- card_data = response.json()
122
- if card_data.get("code") == 0 and "items" in card_data.get("data", {}):
123
- items = card_data["data"]["items"]
124
- if items:
125
- item = items[0]
126
- provider_bin = None
127
- if item.get("provider"):
128
- parts = item["provider"].split('_')
129
- if len(parts) > 1 and parts[-1].isdigit():
130
- provider_bin = parts[-1]
131
-
132
- processed_card_info = {
133
- "provider_bin": provider_bin,
134
- "card_last_four_digits": item.get("card_last_four_digits"),
135
- "consumption_limit": item.get("consumption_limit"),
136
- "status": item.get("status"),
137
- "name": item.get("name")
138
- }
139
- return processed_card_info, None
140
- else:
141
- return None, None
142
- elif card_data.get("code") == 0 and not card_data.get("data", {}).get("items"):
143
- return None, None
144
- else:
145
- error_message = card_data.get('message', "未知卡片信息错误")
146
- if card_data.get("code") == 0 and not card_data.get("data"):
147
- error_message = "响应成功但未返回卡片数据。"
148
- return None, f"{error_message} (Code: {card_data.get('code')})"
149
- except requests.exceptions.Timeout:
150
- return None, "获取卡片信息请求超时。"
151
- except requests.exceptions.RequestException as e:
152
- return None, f"获取卡片信息请求错误: {str(e)}"
153
- except json.JSONDecodeError:
154
- return None, "卡片信息响应不是有效的 JSON。"
155
-
156
- def login_and_store_token(email):
157
- global accounts_data
158
- with data_lock:
159
- account_info = accounts_data.get(email)
160
- if not account_info:
161
- print(f"错误: 账户 {email} 未找到。")
162
- return
163
-
164
- password = account_info["password"]
165
- print(f"[{datetime.now()}] 尝试为账户 {email} 登录...")
166
-
167
- token, error = api_login(email, password)
168
-
169
- with data_lock:
170
- accounts_data[email]["last_login_attempt"] = datetime.now()
171
- if token:
172
- accounts_data[email]["token"] = token
173
- accounts_data[email]["last_login_success"] = True
174
- accounts_data[email]["login_error"] = None
175
- print(f"[{datetime.now()}] 账户 {email} 登录成功。")
176
- else:
177
- accounts_data[email]["token"] = None
178
- accounts_data[email]["last_login_success"] = False
179
- accounts_data[email]["login_error"] = error
180
- print(f"[{datetime.now()}] 账户 {email} 登录失败: {error}")
181
-
182
- def fetch_and_store_profile(email):
183
- global accounts_data
184
- with data_lock:
185
- account_info = accounts_data.get(email)
186
- if not account_info:
187
- print(f"错误: 账户 {email} 未找到 (fetch_and_store_profile)。")
188
- return
189
- token = account_info.get("token")
190
-
191
- if not token:
192
- print(f"[{datetime.now()}] 账户 {email} 没有有效的 token,跳过获取 Profile。")
193
- with data_lock:
194
- accounts_data[email]["last_profile_attempt"] = datetime.now()
195
- accounts_data[email]["last_profile_success"] = False
196
- accounts_data[email]["profile_error"] = "无有效 Token"
197
- accounts_data[email]["profile"] = None
198
- return
199
-
200
- print(f"[{datetime.now()}] 尝试为账户 {email} 获取 Profile...")
201
- profile, error = get_api_profile(email, token)
202
-
203
- with data_lock:
204
- accounts_data[email]["last_profile_attempt"] = datetime.now()
205
- if profile:
206
- accounts_data[email]["profile"] = profile
207
- accounts_data[email]["last_profile_success"] = True
208
- accounts_data[email]["profile_error"] = None
209
- print(f"[{datetime.now()}] 账户 {email} 获取 Profile 成功。")
210
- else:
211
- accounts_data[email]["profile"] = None
212
- accounts_data[email]["last_profile_success"] = False
213
- accounts_data[email]["profile_error"] = error
214
- print(f"[{datetime.now()}] 账户 {email} 获取 Profile 失败: {error}")
215
- if error and ("token" in error.lower() or "auth" in error.lower() or "登录" in error.lower()):
216
- print(f"[{datetime.now()}] 账户 {email} 获取 Profile 失败,疑似 Token 失效,将尝试重新登录。")
217
- accounts_data[email]["token"] = None
218
- return
219
-
220
- print(f"[{datetime.now()}] Profile 获取成功,继续为账户 {email} 获取卡片信息...")
221
- cards_info, card_error = get_api_card_info(email, token)
222
-
223
- with data_lock:
224
- accounts_data[email]["last_card_info_attempt"] = datetime.now()
225
- if cards_info:
226
- accounts_data[email]["cards_info"] = cards_info
227
- accounts_data[email]["last_card_info_success"] = True
228
- accounts_data[email]["card_info_error"] = None
229
- print(f"[{datetime.now()}] 账户 {email} 获取卡片信息成功。")
230
- elif card_error:
231
- accounts_data[email]["cards_info"] = None
232
- accounts_data[email]["last_card_info_success"] = False
233
- accounts_data[email]["card_info_error"] = card_error
234
- print(f"[{datetime.now()}] 账户 {email} 获取卡片信息失败: {card_error}")
235
- else:
236
- accounts_data[email]["cards_info"] = None
237
- accounts_data[email]["last_card_info_success"] = True
238
- accounts_data[email]["card_info_error"] = None
239
- print(f"[{datetime.now()}] 账户 {email} 获取卡片信息成功,但无卡片数据。")
240
-
241
- def initial_login_all_accounts():
242
- print("程序启动,开始为所有账户执行初始登录...")
243
- threads = []
244
- with data_lock:
245
- emails_to_login = list(accounts_data.keys())
246
-
247
- for email in emails_to_login:
248
- thread = threading.Thread(target=login_and_store_token, args=(email,))
249
- threads.append(thread)
250
- thread.start()
251
- for thread in threads:
252
- thread.join()
253
- print("所有账户初始登录尝试完成。")
254
-
255
- def scheduled_login_all_accounts():
256
- print(f"[{datetime.now()}] 定时任务:开始为所有账户重新登录...")
257
- threads = []
258
- with data_lock:
259
- emails_to_login = list(accounts_data.keys())
260
-
261
- for email in emails_to_login:
262
- thread = threading.Thread(target=login_and_store_token, args=(email,))
263
- threads.append(thread)
264
- thread.start()
265
- for thread in threads:
266
- thread.join()
267
- print(f"[{datetime.now()}] 定时任务:所有账户重新登录尝试完成。")
268
- scheduled_fetch_all_profiles()
269
-
270
- def scheduled_fetch_all_profiles():
271
- print(f"[{datetime.now()}] 定时任务:开始为所有账户获取 Profile...")
272
- threads = []
273
- with data_lock:
274
- emails_to_fetch = list(accounts_data.keys())
275
-
276
- for email in emails_to_fetch:
277
- thread = threading.Thread(target=fetch_and_store_profile, args=(email,))
278
- threads.append(thread)
279
- thread.start()
280
- for thread in threads:
281
- thread.join()
282
- print(f"[{datetime.now()}] 定时任务:所有账户获取 Profile 尝试完成。")
283
-
284
- LOGIN_FORM_HTML = """
285
- <!DOCTYPE html>
286
- <html lang="zh-CN">
287
- <head>
288
- <meta charset="UTF-8">
289
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
290
- <title>访问授权</title>
291
- <style>
292
- body {
293
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
294
- display: flex;
295
- justify-content: center;
296
- align-items: center;
297
- height: 100vh;
298
- margin: 0;
299
- background-color: #fff;
300
- color: #000;
301
- }
302
- .login-container {
303
- background-color: #fff;
304
- padding: 40px;
305
- border-radius: 8px;
306
- box-shadow: 0 8px 30px rgba(0,0,0,0.1);
307
- width: 100%;
308
- max-width: 360px;
309
- border: 1px solid #eaeaea;
310
- }
311
- h2 {
312
- text-align: center;
313
- color: #000;
314
- font-weight: 600;
315
- margin-bottom: 30px;
316
- }
317
- label {
318
- display: block;
319
- margin-bottom: 8px;
320
- color: #444;
321
- font-size: 14px;
322
- }
323
- input[type="password"] {
324
- width: 100%;
325
- padding: 12px;
326
- margin-bottom: 20px;
327
- border: 1px solid #ccc;
328
- border-radius: 6px;
329
- box-sizing: border-box;
330
- background-color: #fff;
331
- color: #000;
332
- font-size: 16px;
333
- }
334
- input[type="password"]:focus {
335
- border-color: #999;
336
- outline: none;
337
- box-shadow: 0 0 0 2px rgba(0,0,0,0.05);
338
- }
339
- input[type="submit"] {
340
- width: 100%;
341
- padding: 12px;
342
- background-color: #000;
343
- color: #fff;
344
- border: none;
345
- border-radius: 6px;
346
- cursor: pointer;
347
- font-size: 16px;
348
- font-weight: 500;
349
- transition: background-color 0.2s ease;
350
- }
351
- input[type="submit"]:hover {
352
- background-color: #333;
353
- }
354
- .error {
355
- color: #e53e3e;
356
- text-align: center;
357
- margin-bottom: 15px;
358
- font-size: 14px;
359
- }
360
- </style>
361
- </head>
362
- <body>
363
- <div class="login-container">
364
- <h2>授权访问</h2>
365
- {% if error %}
366
- <p class="error">{{ error }}</p>
367
- {% endif %}
368
- <form method="post">
369
- <label for="password">访问密码:</label>
370
- <input type="password" id="password" name="password" required autofocus>
371
- <input type="submit" value="继续">
372
- </form>
373
- </div>
374
- </body>
375
- </html>
376
- """
377
-
378
- @app.route('/', methods=['GET', 'POST'])
379
- def login_frontend():
380
- if not FRONTEND_PASSWORD:
381
- return "错误: 前端密码 (PASSWORD 环境变量) 未设置。", 500
382
-
383
- if 'logged_in' in session and session['logged_in']:
384
- return redirect(url_for('dashboard'))
385
-
386
- error = None
387
- if request.method == 'POST':
388
- entered_password = request.form.get('password')
389
- if entered_password == FRONTEND_PASSWORD:
390
- session['logged_in'] = True
391
- return redirect(url_for('dashboard'))
392
- else:
393
- error = "密码错误!"
394
- return render_template_string(LOGIN_FORM_HTML, error=error)
395
-
396
- @app.route('/dashboard')
397
- def dashboard():
398
- if not ('logged_in' in session and session['logged_in']):
399
- return redirect(url_for('login_frontend'))
400
- return render_template_string(open('templates/dashboard.html', encoding='utf-8').read())
401
-
402
- @app.route('/logout')
403
- def logout():
404
- session.pop('logged_in', None)
405
- return redirect(url_for('login_frontend'))
406
-
407
- @app.route('/api/data', methods=['GET'])
408
- def get_all_data():
409
- if not ('logged_in' in session and session['logged_in']):
410
- return jsonify({"error": "未授权访问"}), 401
411
-
412
- with data_lock:
413
- display_data = {}
414
- for email, data in accounts_data.items():
415
- display_data[email] = {
416
- "profile": data.get("profile"),
417
- "last_login_success": data.get("last_login_success"),
418
- "last_profile_success": data.get("last_profile_success"),
419
- "last_login_attempt": data.get("last_login_attempt").isoformat() if data.get("last_login_attempt") else None,
420
- "last_profile_attempt": data.get("last_profile_attempt").isoformat() if data.get("last_profile_attempt") else None,
421
- "login_error": data.get("login_error"),
422
- "profile_error": data.get("profile_error"),
423
- "token_present": bool(data.get("token")),
424
- "cards_info": data.get("cards_info"),
425
- "last_card_info_success": data.get("last_card_info_success"),
426
- "last_card_info_attempt": data.get("last_card_info_attempt").isoformat() if data.get("last_card_info_attempt") else None,
427
- "card_info_error": data.get("card_info_error")
428
- }
429
- return jsonify(display_data)
430
-
431
- @app.route('/api/refresh', methods=['POST'])
432
- def manual_refresh_all_data():
433
- if not ('logged_in' in session and session['logged_in']):
434
- return jsonify({"error": "未授权访问"}), 401
435
-
436
- print(f"[{datetime.now()}] 手动触发数据刷新...")
437
- threading.Thread(target=scheduled_login_all_accounts).start()
438
- return jsonify({"message": "刷新任务已启动,请稍后查看数据。"}), 202
439
-
440
- if __name__ == '__main__':
441
- if not FRONTEND_PASSWORD:
442
- print("警告: PASSWORD 环境变量未设置,前端将无法登录。")
443
- if not parse_accounts():
444
- print("由于账户加载失败,程序将退出。请检查 ACCOUNTS 环境变量。")
445
- else:
446
- initial_login_all_accounts()
447
- scheduled_fetch_all_profiles()
448
-
449
- scheduler.add_job(scheduled_login_all_accounts, 'interval', days=3, id='job_login_all')
450
- scheduler.add_job(scheduled_fetch_all_profiles, 'interval', minutes=30, id='job_fetch_profiles')
451
-
452
- try:
453
- scheduler.start()
454
- print("定时任务已启动。")
455
- print(f"APScheduler jobs: {scheduler.get_jobs()}")
456
- except Exception as e:
457
- print(f"启动 APScheduler 失败: {e}")
458
-
459
- is_hf_space = os.getenv("SPACE_ID") is not None
460
- if not is_hf_space:
461
- app.run(host='0.0.0.0', port=7860, debug=True)
462
-
463
- else:
464
- if FRONTEND_PASSWORD and parse_accounts():
465
- initial_login_all_accounts()
466
- scheduled_fetch_all_profiles()
467
- if not scheduler.running:
468
- scheduler.add_job(scheduled_login_all_accounts, 'interval', days=3, id='job_login_all_gunicorn')
469
- scheduler.add_job(scheduled_fetch_all_profiles, 'interval', minutes=30, id='job_fetch_profiles_gunicorn')
470
- try:
471
- scheduler.start()
472
- print("APScheduler (Gunicorn) jobs started.")
473
- except Exception as e:
474
- print(f"Failed to start APScheduler (Gunicorn): {e}")
475
- else:
476
- print("Gunicorn: 未能正确初始化账户或密码未设置。")
 
 
 
1
+ import os
2
+ import json
3
+ import threading
4
+ from datetime import datetime
5
+ from flask import Flask, request, jsonify, render_template_string, redirect, url_for, session
6
+ import requests
7
+ from apscheduler.schedulers.background import BackgroundScheduler
8
+ from dotenv import load_dotenv
9
+
10
+ load_dotenv()
11
+
12
+ app = Flask(__name__)
13
+ app.secret_key = os.getenv("SECRET_KEY")
14
+ if not app.secret_key:
15
+ print("警告: SECRET_KEY 环境变量未设置。将使用默认的、不安全的密钥。请在生产环境中设置一个安全的 SECRET_KEY。")
16
+ app.secret_key = "dev_secret_key_for_testing_only_change_me"
17
+
18
+ LOGIN_URL = "https://api-card.infini.money/user/login"
19
+ PROFILE_URL = "https://api-card.infini.money/user/profile"
20
+ CARD_INFO_URL = "https://api-card.infini.money/card/info"
21
+ FRONTEND_PASSWORD = os.getenv("PASSWORD")
22
+ ACCOUNTS_JSON = os.getenv("ACCOUNTS")
23
+
24
+ accounts_data = {}
25
+ scheduler = BackgroundScheduler(daemon=True)
26
+ data_lock = threading.Lock()
27
+
28
+ def parse_accounts():
29
+ global accounts_data
30
+ if not ACCOUNTS_JSON:
31
+ print("错误: ACCOUNTS 环境变量未设置。")
32
+ return False
33
+ try:
34
+ accounts_list = json.loads(ACCOUNTS_JSON)
35
+ if not isinstance(accounts_list, list):
36
+ print("错误: ACCOUNTS 环境变量必须是一个 JSON 数组。")
37
+ return False
38
+
39
+ temp_accounts_data = {}
40
+ for acc in accounts_list:
41
+ if isinstance(acc, dict) and "email" in acc and "password" in acc:
42
+ temp_accounts_data[acc["email"]] = {
43
+ "password": acc["password"],
44
+ "token": None,
45
+ "profile": None,
46
+ "last_login_success": None,
47
+ "last_profile_success": None,
48
+ "last_login_attempt": None,
49
+ "last_profile_attempt": None,
50
+ "login_error": None,
51
+ "profile_error": None,
52
+ "cards_info": None,
53
+ "last_card_info_success": None,
54
+ "last_card_info_attempt": None,
55
+ "card_info_error": None,
56
+ }
57
+ else:
58
+ print(f"警告: ACCOUNTS 中的条目格式不正确: {acc}")
59
+
60
+ with data_lock:
61
+ accounts_data = temp_accounts_data
62
+ print(f"成功加载 {len(accounts_data)} 个账户。")
63
+ return True
64
+ except json.JSONDecodeError:
65
+ print("错误: ACCOUNTS 环境变量不是有效的 JSON 格式。")
66
+ return False
67
+
68
+ def api_login(email, password):
69
+ payload = {"email": email, "password": password}
70
+ try:
71
+ response = requests.post(LOGIN_URL, json=payload, timeout=10)
72
+ response.raise_for_status()
73
+ data = response.json()
74
+ cookies = response.cookies
75
+ jwt_token = cookies.get("jwt_token")
76
+
77
+ if data.get("code") == 0 and jwt_token:
78
+ return jwt_token, None
79
+ else:
80
+ error_message = data.get('message', "未知登录错误")
81
+ if data.get("code") == 0 and not jwt_token:
82
+ error_message = "响应成功但未返回 jwt_token。"
83
+ return None, f"{error_message} (Code: {data.get('code')})"
84
+ except requests.exceptions.Timeout:
85
+ return None, "登录请求超时。"
86
+ except requests.exceptions.RequestException as e:
87
+ return None, f"登录请求错误: {str(e)}"
88
+ except json.JSONDecodeError:
89
+ return None, "登录响应不是有效的 JSON。"
90
+
91
+ def get_api_profile(email, token):
92
+ if not token:
93
+ return None, "Token 为空,无法获取 Profile。"
94
+
95
+ cookies = {"jwt_token": token}
96
+ try:
97
+ response = requests.get(PROFILE_URL, cookies=cookies, timeout=10)
98
+ response.raise_for_status()
99
+ profile_data = response.json()
100
+ if profile_data.get("code") == 0 and profile_data.get("data"):
101
+ return profile_data.get("data"), None
102
+ else:
103
+ error_message = profile_data.get('message', "未知 Profile 错误")
104
+ if profile_data.get("code") == 0 and not profile_data.get("data"):
105
+ error_message = "响应成功但未返回 Profile 数据。"
106
+ return None, f"{error_message} (Code: {profile_data.get('code')})"
107
+ except requests.exceptions.Timeout:
108
+ return None, "获取 Profile 请求超时。"
109
+ except requests.exceptions.RequestException as e:
110
+ return None, f"获取 Profile 请求错误: {str(e)}"
111
+ except json.JSONDecodeError:
112
+ return None, "Profile 响应不是有效的 JSON。"
113
+
114
+ def get_api_card_info(email, token):
115
+ if not token:
116
+ return None, "Token 为空,无法获取卡片信息。"
117
+
118
+ cookies = {"jwt_token": token}
119
+ print(f"[{datetime.now()}] 尝试为账户 {email} 获取卡片信息...")
120
+ try:
121
+ response = requests.get(CARD_INFO_URL, cookies=cookies, timeout=10)
122
+ response.raise_for_status()
123
+ card_data = response.json()
124
+ if card_data.get("code") == 0 and "items" in card_data.get("data", {}):
125
+ items = card_data["data"]["items"]
126
+ if items:
127
+ item = items[0]
128
+ provider_bin = None
129
+ if item.get("provider"):
130
+ parts = item["provider"].split('_')
131
+ if len(parts) > 1 and parts[-1].isdigit():
132
+ provider_bin = parts[-1]
133
+
134
+ processed_card_info = {
135
+ "provider_bin": provider_bin,
136
+ "card_last_four_digits": item.get("card_last_four_digits"),
137
+ "consumption_limit": item.get("consumption_limit"),
138
+ "status": item.get("status"),
139
+ "name": item.get("name")
140
+ }
141
+ return processed_card_info, None
142
+ else:
143
+ return None, None
144
+ elif card_data.get("code") == 0 and not card_data.get("data", {}).get("items"):
145
+ return None, None
146
+ else:
147
+ error_message = card_data.get('message', "未知卡片信息错误")
148
+ if card_data.get("code") == 0 and not card_data.get("data"):
149
+ error_message = "响应成功但未返回卡片数据。"
150
+ return None, f"{error_message} (Code: {card_data.get('code')})"
151
+ except requests.exceptions.Timeout:
152
+ return None, "获取卡片信息请求超时。"
153
+ except requests.exceptions.RequestException as e:
154
+ return None, f"获取卡片信息请求错误: {str(e)}"
155
+ except json.JSONDecodeError:
156
+ return None, "卡片信息响应不是有效的 JSON。"
157
+
158
+ def login_and_store_token(email):
159
+ global accounts_data
160
+ with data_lock:
161
+ account_info = accounts_data.get(email)
162
+ if not account_info:
163
+ print(f"错误: 账户 {email} 未找到。")
164
+ return
165
+
166
+ password = account_info["password"]
167
+ print(f"[{datetime.now()}] 尝试为账户 {email} 登录...")
168
+
169
+ token, error = api_login(email, password)
170
+
171
+ with data_lock:
172
+ accounts_data[email]["last_login_attempt"] = datetime.now()
173
+ if token:
174
+ accounts_data[email]["token"] = token
175
+ accounts_data[email]["last_login_success"] = True
176
+ accounts_data[email]["login_error"] = None
177
+ print(f"[{datetime.now()}] 账户 {email} 登录成功。")
178
+ else:
179
+ accounts_data[email]["token"] = None
180
+ accounts_data[email]["last_login_success"] = False
181
+ accounts_data[email]["login_error"] = error
182
+ print(f"[{datetime.now()}] 账户 {email} 登录失败: {error}")
183
+
184
+ def fetch_and_store_profile(email):
185
+ global accounts_data
186
+ with data_lock:
187
+ account_info = accounts_data.get(email)
188
+ if not account_info:
189
+ print(f"错误: 账户 {email} 未找到 (fetch_and_store_profile)。")
190
+ return
191
+ token = account_info.get("token")
192
+
193
+ if not token:
194
+ print(f"[{datetime.now()}] 账户 {email} 没有有效的 token,跳过获取 Profile。")
195
+ with data_lock:
196
+ accounts_data[email]["last_profile_attempt"] = datetime.now()
197
+ accounts_data[email]["last_profile_success"] = False
198
+ accounts_data[email]["profile_error"] = "无有效 Token"
199
+ accounts_data[email]["profile"] = None
200
+ return
201
+
202
+ print(f"[{datetime.now()}] 尝试为账户 {email} 获取 Profile...")
203
+ profile, error = get_api_profile(email, token)
204
+
205
+ with data_lock:
206
+ accounts_data[email]["last_profile_attempt"] = datetime.now()
207
+ if profile:
208
+ accounts_data[email]["profile"] = profile
209
+ accounts_data[email]["last_profile_success"] = True
210
+ accounts_data[email]["profile_error"] = None
211
+ print(f"[{datetime.now()}] 账户 {email} 获取 Profile 成功。")
212
+ else:
213
+ accounts_data[email]["profile"] = None
214
+ accounts_data[email]["last_profile_success"] = False
215
+ accounts_data[email]["profile_error"] = error
216
+ print(f"[{datetime.now()}] 账户 {email} 获取 Profile 失败: {error}")
217
+ if error and ("token" in error.lower() or "auth" in error.lower() or "登录" in error.lower()):
218
+ print(f"[{datetime.now()}] 账户 {email} 获取 Profile 失败,疑似 Token 失效,将尝试重新登录。")
219
+ accounts_data[email]["token"] = None
220
+ return
221
+
222
+ print(f"[{datetime.now()}] Profile 获取成功,继续为账户 {email} 获取卡片信息...")
223
+ cards_info, card_error = get_api_card_info(email, token)
224
+
225
+ with data_lock:
226
+ accounts_data[email]["last_card_info_attempt"] = datetime.now()
227
+ if cards_info:
228
+ accounts_data[email]["cards_info"] = cards_info
229
+ accounts_data[email]["last_card_info_success"] = True
230
+ accounts_data[email]["card_info_error"] = None
231
+ print(f"[{datetime.now()}] 账户 {email} 获取卡片信息成功。")
232
+ elif card_error:
233
+ accounts_data[email]["cards_info"] = None
234
+ accounts_data[email]["last_card_info_success"] = False
235
+ accounts_data[email]["card_info_error"] = card_error
236
+ print(f"[{datetime.now()}] 账户 {email} 获取卡片信息失败: {card_error}")
237
+ else:
238
+ accounts_data[email]["cards_info"] = None
239
+ accounts_data[email]["last_card_info_success"] = True
240
+ accounts_data[email]["card_info_error"] = None
241
+ print(f"[{datetime.now()}] 账户 {email} 获取卡片信息成功,但无卡片数据。")
242
+
243
+ def initial_login_all_accounts():
244
+ print("程序启动,开始为所有账户执行初始登录...")
245
+ threads = []
246
+ with data_lock:
247
+ emails_to_login = list(accounts_data.keys())
248
+
249
+ for email in emails_to_login:
250
+ thread = threading.Thread(target=login_and_store_token, args=(email,))
251
+ threads.append(thread)
252
+ thread.start()
253
+ for thread in threads:
254
+ thread.join()
255
+ print("所有账户初始登录尝试完成。")
256
+
257
+ def scheduled_login_all_accounts():
258
+ print(f"[{datetime.now()}] 定时任务:开始为所有账户重新登录...")
259
+ threads = []
260
+ with data_lock:
261
+ emails_to_login = list(accounts_data.keys())
262
+
263
+ for email in emails_to_login:
264
+ thread = threading.Thread(target=login_and_store_token, args=(email,))
265
+ threads.append(thread)
266
+ thread.start()
267
+ for thread in threads:
268
+ thread.join()
269
+ print(f"[{datetime.now()}] 定时任务:所有账户重新登录尝试完成。")
270
+ scheduled_fetch_all_profiles()
271
+
272
+ def scheduled_fetch_all_profiles():
273
+ print(f"[{datetime.now()}] 定时任务:开始为所有账户获取 Profile...")
274
+ threads = []
275
+ with data_lock:
276
+ emails_to_fetch = list(accounts_data.keys())
277
+
278
+ for email in emails_to_fetch:
279
+ thread = threading.Thread(target=fetch_and_store_profile, args=(email,))
280
+ threads.append(thread)
281
+ thread.start()
282
+ for thread in threads:
283
+ thread.join()
284
+ print(f"[{datetime.now()}] 定时任务:所有账户获取 Profile 尝试完成。")
285
+
286
+ LOGIN_FORM_HTML = """
287
+ <!DOCTYPE html>
288
+ <html lang="zh-CN">
289
+ <head>
290
+ <meta charset="UTF-8">
291
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
292
+ <title>访问授权</title>
293
+ <style>
294
+ body {
295
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
296
+ display: flex;
297
+ justify-content: center;
298
+ align-items: center;
299
+ height: 100vh;
300
+ margin: 0;
301
+ background-color: #fff;
302
+ color: #000;
303
+ }
304
+ .login-container {
305
+ background-color: #fff;
306
+ padding: 40px;
307
+ border-radius: 8px;
308
+ box-shadow: 0 8px 30px rgba(0,0,0,0.1);
309
+ width: 100%;
310
+ max-width: 360px;
311
+ border: 1px solid #eaeaea;
312
+ }
313
+ h2 {
314
+ text-align: center;
315
+ color: #000;
316
+ font-weight: 600;
317
+ margin-bottom: 30px;
318
+ }
319
+ label {
320
+ display: block;
321
+ margin-bottom: 8px;
322
+ color: #444;
323
+ font-size: 14px;
324
+ }
325
+ input[type="password"] {
326
+ width: 100%;
327
+ padding: 12px;
328
+ margin-bottom: 20px;
329
+ border: 1px solid #ccc;
330
+ border-radius: 6px;
331
+ box-sizing: border-box;
332
+ background-color: #fff;
333
+ color: #000;
334
+ font-size: 16px;
335
+ }
336
+ input[type="password"]:focus {
337
+ border-color: #999;
338
+ outline: none;
339
+ box-shadow: 0 0 0 2px rgba(0,0,0,0.05);
340
+ }
341
+ input[type="submit"] {
342
+ width: 100%;
343
+ padding: 12px;
344
+ background-color: #000;
345
+ color: #fff;
346
+ border: none;
347
+ border-radius: 6px;
348
+ cursor: pointer;
349
+ font-size: 16px;
350
+ font-weight: 500;
351
+ transition: background-color 0.2s ease;
352
+ }
353
+ input[type="submit"]:hover {
354
+ background-color: #333;
355
+ }
356
+ .error {
357
+ color: #e53e3e;
358
+ text-align: center;
359
+ margin-bottom: 15px;
360
+ font-size: 14px;
361
+ }
362
+ </style>
363
+ </head>
364
+ <body>
365
+ <div class="login-container">
366
+ <h2>授权访问</h2>
367
+ {% if error %}
368
+ <p class="error">{{ error }}</p>
369
+ {% endif %}
370
+ <form method="post">
371
+ <label for="password">访问密码:</label>
372
+ <input type="password" id="password" name="password" required autofocus>
373
+ <input type="submit" value="继续">
374
+ </form>
375
+ </div>
376
+ </body>
377
+ </html>
378
+ """
379
+
380
+ @app.route('/', methods=['GET', 'POST'])
381
+ def login_frontend():
382
+ if not FRONTEND_PASSWORD:
383
+ return "错误: 前端密码 (PASSWORD 环境变量) 未设置。", 500
384
+
385
+ if 'logged_in' in session and session['logged_in']:
386
+ return redirect(url_for('dashboard'))
387
+
388
+ error = None
389
+ if request.method == 'POST':
390
+ entered_password = request.form.get('password')
391
+ if entered_password == FRONTEND_PASSWORD:
392
+ session['logged_in'] = True
393
+ return redirect(url_for('dashboard'))
394
+ else:
395
+ error = "密码错误!"
396
+ return render_template_string(LOGIN_FORM_HTML, error=error)
397
+
398
+ @app.route('/dashboard')
399
+ def dashboard():
400
+ if not ('logged_in' in session and session['logged_in']):
401
+ return redirect(url_for('login_frontend'))
402
+ return render_template_string(open('templates/dashboard.html', encoding='utf-8').read())
403
+
404
+ @app.route('/logout')
405
+ def logout():
406
+ session.pop('logged_in', None)
407
+ return redirect(url_for('login_frontend'))
408
+
409
+ @app.route('/api/data', methods=['GET'])
410
+ def get_all_data():
411
+ if not ('logged_in' in session and session['logged_in']):
412
+ return jsonify({"error": "未授权访问"}), 401
413
+
414
+ with data_lock:
415
+ display_data = {}
416
+ for email, data in accounts_data.items():
417
+ display_data[email] = {
418
+ "profile": data.get("profile"),
419
+ "last_login_success": data.get("last_login_success"),
420
+ "last_profile_success": data.get("last_profile_success"),
421
+ "last_login_attempt": data.get("last_login_attempt").isoformat() if data.get("last_login_attempt") else None,
422
+ "last_profile_attempt": data.get("last_profile_attempt").isoformat() if data.get("last_profile_attempt") else None,
423
+ "login_error": data.get("login_error"),
424
+ "profile_error": data.get("profile_error"),
425
+ "token_present": bool(data.get("token")),
426
+ "cards_info": data.get("cards_info"),
427
+ "last_card_info_success": data.get("last_card_info_success"),
428
+ "last_card_info_attempt": data.get("last_card_info_attempt").isoformat() if data.get("last_card_info_attempt") else None,
429
+ "card_info_error": data.get("card_info_error")
430
+ }
431
+ return jsonify(display_data)
432
+
433
+ @app.route('/api/refresh', methods=['POST'])
434
+ def manual_refresh_all_data():
435
+ if not ('logged_in' in session and session['logged_in']):
436
+ return jsonify({"error": "未授权访问"}), 401
437
+
438
+ print(f"[{datetime.now()}] 手动触发数据刷新...")
439
+ threading.Thread(target=scheduled_login_all_accounts).start()
440
+ return jsonify({"message": "刷新任务已启动,请稍后查看数据。"}), 202
441
+
442
+ if __name__ == '__main__':
443
+ if not FRONTEND_PASSWORD:
444
+ print("警告: PASSWORD 环境变量未设置,前端将无法登录。")
445
+ if not parse_accounts():
446
+ print("由于账户加载失败,程序将退出。请检查 ACCOUNTS 环境变量。")
447
+ else:
448
+ initial_login_all_accounts()
449
+ scheduled_fetch_all_profiles()
450
+
451
+ scheduler.add_job(scheduled_login_all_accounts, 'interval', days=3, id='job_login_all')
452
+ scheduler.add_job(scheduled_fetch_all_profiles, 'interval', minutes=30, id='job_fetch_profiles')
453
+
454
+ try:
455
+ scheduler.start()
456
+ print("定时任务已启动。")
457
+ print(f"APScheduler jobs: {scheduler.get_jobs()}")
458
+ except Exception as e:
459
+ print(f"启动 APScheduler 失败: {e}")
460
+
461
+ is_hf_space = os.getenv("SPACE_ID") is not None
462
+ if not is_hf_space:
463
+ app.run(host='0.0.0.0', port=7860, debug=True)
464
+
465
+ else:
466
+ if FRONTEND_PASSWORD and parse_accounts():
467
+ initial_login_all_accounts()
468
+ scheduled_fetch_all_profiles()
469
+ if not scheduler.running:
470
+ scheduler.add_job(scheduled_login_all_accounts, 'interval', days=3, id='job_login_all_gunicorn')
471
+ scheduler.add_job(scheduled_fetch_all_profiles, 'interval', minutes=30, id='job_fetch_profiles_gunicorn')
472
+ try:
473
+ scheduler.start()
474
+ print("APScheduler (Gunicorn) jobs started.")
475
+ except Exception as e:
476
+ print(f"Failed to start APScheduler (Gunicorn): {e}")
477
+ else:
478
+ print("Gunicorn: 未能正确初始化账户或密码未设置。")