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

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +479 -478
  2. templates/dashboard.html +237 -85
app.py CHANGED
@@ -1,478 +1,479 @@
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: 未能正确初始化账户或密码未设置。")
 
 
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
+ "available_balance": item.get("available_balance")
141
+ }
142
+ return processed_card_info, None
143
+ else:
144
+ return None, None
145
+ elif card_data.get("code") == 0 and not card_data.get("data", {}).get("items"):
146
+ return None, None
147
+ else:
148
+ error_message = card_data.get('message', "未知卡片信息错误")
149
+ if card_data.get("code") == 0 and not card_data.get("data"):
150
+ error_message = "响应成功但未返回卡片数据。"
151
+ return None, f"{error_message} (Code: {card_data.get('code')})"
152
+ except requests.exceptions.Timeout:
153
+ return None, "获取卡片信息请求超时。"
154
+ except requests.exceptions.RequestException as e:
155
+ return None, f"获取卡片信息请求错误: {str(e)}"
156
+ except json.JSONDecodeError:
157
+ return None, "卡片信息响应不是有效的 JSON。"
158
+
159
+ def login_and_store_token(email):
160
+ global accounts_data
161
+ with data_lock:
162
+ account_info = accounts_data.get(email)
163
+ if not account_info:
164
+ print(f"错误: 账户 {email} 未找到。")
165
+ return
166
+
167
+ password = account_info["password"]
168
+ print(f"[{datetime.now()}] 尝试为账户 {email} 登录...")
169
+
170
+ token, error = api_login(email, password)
171
+
172
+ with data_lock:
173
+ accounts_data[email]["last_login_attempt"] = datetime.now()
174
+ if token:
175
+ accounts_data[email]["token"] = token
176
+ accounts_data[email]["last_login_success"] = True
177
+ accounts_data[email]["login_error"] = None
178
+ print(f"[{datetime.now()}] 账户 {email} 登录成功。")
179
+ else:
180
+ accounts_data[email]["token"] = None
181
+ accounts_data[email]["last_login_success"] = False
182
+ accounts_data[email]["login_error"] = error
183
+ print(f"[{datetime.now()}] 账户 {email} 登录失败: {error}")
184
+
185
+ def fetch_and_store_profile(email):
186
+ global accounts_data
187
+ with data_lock:
188
+ account_info = accounts_data.get(email)
189
+ if not account_info:
190
+ print(f"错误: 账户 {email} 未找到 (fetch_and_store_profile)。")
191
+ return
192
+ token = account_info.get("token")
193
+
194
+ if not token:
195
+ print(f"[{datetime.now()}] 账户 {email} 没有有效的 token,跳过获取 Profile。")
196
+ with data_lock:
197
+ accounts_data[email]["last_profile_attempt"] = datetime.now()
198
+ accounts_data[email]["last_profile_success"] = False
199
+ accounts_data[email]["profile_error"] = "无有效 Token"
200
+ accounts_data[email]["profile"] = None
201
+ return
202
+
203
+ print(f"[{datetime.now()}] 尝试为账户 {email} 获取 Profile...")
204
+ profile, error = get_api_profile(email, token)
205
+
206
+ with data_lock:
207
+ accounts_data[email]["last_profile_attempt"] = datetime.now()
208
+ if profile:
209
+ accounts_data[email]["profile"] = profile
210
+ accounts_data[email]["last_profile_success"] = True
211
+ accounts_data[email]["profile_error"] = None
212
+ print(f"[{datetime.now()}] 账户 {email} 获取 Profile 成功。")
213
+ else:
214
+ accounts_data[email]["profile"] = None
215
+ accounts_data[email]["last_profile_success"] = False
216
+ accounts_data[email]["profile_error"] = error
217
+ print(f"[{datetime.now()}] 账户 {email} 获取 Profile 失败: {error}")
218
+ if error and ("token" in error.lower() or "auth" in error.lower() or "登录" in error.lower()):
219
+ print(f"[{datetime.now()}] 账户 {email} 获取 Profile 失败,疑似 Token 失效,将尝试重新登录。")
220
+ accounts_data[email]["token"] = None
221
+ return
222
+
223
+ print(f"[{datetime.now()}] Profile 获取成功,继续为账户 {email} 获取卡片信息...")
224
+ cards_info, card_error = get_api_card_info(email, token)
225
+
226
+ with data_lock:
227
+ accounts_data[email]["last_card_info_attempt"] = datetime.now()
228
+ if cards_info:
229
+ accounts_data[email]["cards_info"] = cards_info
230
+ accounts_data[email]["last_card_info_success"] = True
231
+ accounts_data[email]["card_info_error"] = None
232
+ print(f"[{datetime.now()}] 账户 {email} 获取卡片信息成功。")
233
+ elif card_error:
234
+ accounts_data[email]["cards_info"] = None
235
+ accounts_data[email]["last_card_info_success"] = False
236
+ accounts_data[email]["card_info_error"] = card_error
237
+ print(f"[{datetime.now()}] 账户 {email} 获取卡片信息失败: {card_error}")
238
+ else:
239
+ accounts_data[email]["cards_info"] = None
240
+ accounts_data[email]["last_card_info_success"] = True
241
+ accounts_data[email]["card_info_error"] = None
242
+ print(f"[{datetime.now()}] 账户 {email} 获取卡片信息成��,但无卡片数据。")
243
+
244
+ def initial_login_all_accounts():
245
+ print("程序启动,开始为所有账户执行初始登录...")
246
+ threads = []
247
+ with data_lock:
248
+ emails_to_login = list(accounts_data.keys())
249
+
250
+ for email in emails_to_login:
251
+ thread = threading.Thread(target=login_and_store_token, args=(email,))
252
+ threads.append(thread)
253
+ thread.start()
254
+ for thread in threads:
255
+ thread.join()
256
+ print("所有账户初始登录尝试完成。")
257
+
258
+ def scheduled_login_all_accounts():
259
+ print(f"[{datetime.now()}] 定时任务:开始为所有账户重新登录...")
260
+ threads = []
261
+ with data_lock:
262
+ emails_to_login = list(accounts_data.keys())
263
+
264
+ for email in emails_to_login:
265
+ thread = threading.Thread(target=login_and_store_token, args=(email,))
266
+ threads.append(thread)
267
+ thread.start()
268
+ for thread in threads:
269
+ thread.join()
270
+ print(f"[{datetime.now()}] 定时任务:所有账户重新登录尝试完成。")
271
+ scheduled_fetch_all_profiles()
272
+
273
+ def scheduled_fetch_all_profiles():
274
+ print(f"[{datetime.now()}] 定时任务:开始为所有账户获取 Profile...")
275
+ threads = []
276
+ with data_lock:
277
+ emails_to_fetch = list(accounts_data.keys())
278
+
279
+ for email in emails_to_fetch:
280
+ thread = threading.Thread(target=fetch_and_store_profile, args=(email,))
281
+ threads.append(thread)
282
+ thread.start()
283
+ for thread in threads:
284
+ thread.join()
285
+ print(f"[{datetime.now()}] 定时任务:所有账户获取 Profile 尝试完成。")
286
+
287
+ LOGIN_FORM_HTML = """
288
+ <!DOCTYPE html>
289
+ <html lang="zh-CN">
290
+ <head>
291
+ <meta charset="UTF-8">
292
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
293
+ <title>访问授权</title>
294
+ <style>
295
+ body {
296
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
297
+ display: flex;
298
+ justify-content: center;
299
+ align-items: center;
300
+ height: 100vh;
301
+ margin: 0;
302
+ background-color: #fff;
303
+ color: #000;
304
+ }
305
+ .login-container {
306
+ background-color: #fff;
307
+ padding: 40px;
308
+ border-radius: 8px;
309
+ box-shadow: 0 8px 30px rgba(0,0,0,0.1);
310
+ width: 100%;
311
+ max-width: 360px;
312
+ border: 1px solid #eaeaea;
313
+ }
314
+ h2 {
315
+ text-align: center;
316
+ color: #000;
317
+ font-weight: 600;
318
+ margin-bottom: 30px;
319
+ }
320
+ label {
321
+ display: block;
322
+ margin-bottom: 8px;
323
+ color: #444;
324
+ font-size: 14px;
325
+ }
326
+ input[type="password"] {
327
+ width: 100%;
328
+ padding: 12px;
329
+ margin-bottom: 20px;
330
+ border: 1px solid #ccc;
331
+ border-radius: 6px;
332
+ box-sizing: border-box;
333
+ background-color: #fff;
334
+ color: #000;
335
+ font-size: 16px;
336
+ }
337
+ input[type="password"]:focus {
338
+ border-color: #999;
339
+ outline: none;
340
+ box-shadow: 0 0 0 2px rgba(0,0,0,0.05);
341
+ }
342
+ input[type="submit"] {
343
+ width: 100%;
344
+ padding: 12px;
345
+ background-color: #000;
346
+ color: #fff;
347
+ border: none;
348
+ border-radius: 6px;
349
+ cursor: pointer;
350
+ font-size: 16px;
351
+ font-weight: 500;
352
+ transition: background-color 0.2s ease;
353
+ }
354
+ input[type="submit"]:hover {
355
+ background-color: #333;
356
+ }
357
+ .error {
358
+ color: #e53e3e;
359
+ text-align: center;
360
+ margin-bottom: 15px;
361
+ font-size: 14px;
362
+ }
363
+ </style>
364
+ </head>
365
+ <body>
366
+ <div class="login-container">
367
+ <h2>授权访问</h2>
368
+ {% if error %}
369
+ <p class="error">{{ error }}</p>
370
+ {% endif %}
371
+ <form method="post">
372
+ <label for="password">访问密码:</label>
373
+ <input type="password" id="password" name="password" required autofocus>
374
+ <input type="submit" value="继续">
375
+ </form>
376
+ </div>
377
+ </body>
378
+ </html>
379
+ """
380
+
381
+ @app.route('/', methods=['GET', 'POST'])
382
+ def login_frontend():
383
+ if not FRONTEND_PASSWORD:
384
+ return "错误: 前端密码 (PASSWORD 环境变量) 未设置。", 500
385
+
386
+ if 'logged_in' in session and session['logged_in']:
387
+ return redirect(url_for('dashboard'))
388
+
389
+ error = None
390
+ if request.method == 'POST':
391
+ entered_password = request.form.get('password')
392
+ if entered_password == FRONTEND_PASSWORD:
393
+ session['logged_in'] = True
394
+ return redirect(url_for('dashboard'))
395
+ else:
396
+ error = "密码错误!"
397
+ return render_template_string(LOGIN_FORM_HTML, error=error)
398
+
399
+ @app.route('/dashboard')
400
+ def dashboard():
401
+ if not ('logged_in' in session and session['logged_in']):
402
+ return redirect(url_for('login_frontend'))
403
+ return render_template_string(open('templates/dashboard.html', encoding='utf-8').read())
404
+
405
+ @app.route('/logout')
406
+ def logout():
407
+ session.pop('logged_in', None)
408
+ return redirect(url_for('login_frontend'))
409
+
410
+ @app.route('/api/data', methods=['GET'])
411
+ def get_all_data():
412
+ if not ('logged_in' in session and session['logged_in']):
413
+ return jsonify({"error": "未授权访问"}), 401
414
+
415
+ with data_lock:
416
+ display_data = {}
417
+ for email, data in accounts_data.items():
418
+ display_data[email] = {
419
+ "profile": data.get("profile"),
420
+ "last_login_success": data.get("last_login_success"),
421
+ "last_profile_success": data.get("last_profile_success"),
422
+ "last_login_attempt": data.get("last_login_attempt").isoformat() if data.get("last_login_attempt") else None,
423
+ "last_profile_attempt": data.get("last_profile_attempt").isoformat() if data.get("last_profile_attempt") else None,
424
+ "login_error": data.get("login_error"),
425
+ "profile_error": data.get("profile_error"),
426
+ "token_present": bool(data.get("token")),
427
+ "cards_info": data.get("cards_info"),
428
+ "last_card_info_success": data.get("last_card_info_success"),
429
+ "last_card_info_attempt": data.get("last_card_info_attempt").isoformat() if data.get("last_card_info_attempt") else None,
430
+ "card_info_error": data.get("card_info_error")
431
+ }
432
+ return jsonify(display_data)
433
+
434
+ @app.route('/api/refresh', methods=['POST'])
435
+ def manual_refresh_all_data():
436
+ if not ('logged_in' in session and session['logged_in']):
437
+ return jsonify({"error": "未授权访问"}), 401
438
+
439
+ print(f"[{datetime.now()}] 手动触发数据刷新...")
440
+ threading.Thread(target=scheduled_login_all_accounts).start()
441
+ return jsonify({"message": "刷新任务已启动,请稍后查看数据。"}), 202
442
+
443
+ if __name__ == '__main__':
444
+ if not FRONTEND_PASSWORD:
445
+ print("警告: PASSWORD 环境变量未设置,前端将无法登录。")
446
+ if not parse_accounts():
447
+ print("由于账户加载失败,程序将退出。请检查 ACCOUNTS 环境变量。")
448
+ else:
449
+ initial_login_all_accounts()
450
+ scheduled_fetch_all_profiles()
451
+
452
+ scheduler.add_job(scheduled_login_all_accounts, 'interval', days=3, id='job_login_all')
453
+ scheduler.add_job(scheduled_fetch_all_profiles, 'interval', minutes=30, id='job_fetch_profiles')
454
+
455
+ try:
456
+ scheduler.start()
457
+ print("定时任务已启动。")
458
+ print(f"APScheduler jobs: {scheduler.get_jobs()}")
459
+ except Exception as e:
460
+ print(f"启动 APScheduler 失败: {e}")
461
+
462
+ is_hf_space = os.getenv("SPACE_ID") is not None
463
+ if not is_hf_space:
464
+ app.run(host='0.0.0.0', port=7860, debug=True)
465
+
466
+ else:
467
+ if FRONTEND_PASSWORD and parse_accounts():
468
+ initial_login_all_accounts()
469
+ scheduled_fetch_all_profiles()
470
+ if not scheduler.running:
471
+ scheduler.add_job(scheduled_login_all_accounts, 'interval', days=3, id='job_login_all_gunicorn')
472
+ scheduler.add_job(scheduled_fetch_all_profiles, 'interval', minutes=30, id='job_fetch_profiles_gunicorn')
473
+ try:
474
+ scheduler.start()
475
+ print("APScheduler (Gunicorn) jobs started.")
476
+ except Exception as e:
477
+ print(f"Failed to start APScheduler (Gunicorn): {e}")
478
+ else:
479
+ print("Gunicorn: 未能正确初始化账户或密码未设置。")
templates/dashboard.html CHANGED
@@ -4,134 +4,248 @@
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
  <title>账户仪表盘</title>
 
 
 
7
  <style>
 
 
 
 
 
 
 
 
 
 
 
 
8
  body {
9
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
10
  margin: 0;
11
- background-color: #f7f7f7;
12
- color: #333;
13
  line-height: 1.6;
 
 
14
  }
15
  .container {
16
  max-width: 1200px;
17
- margin: 30px auto;
18
- padding: 25px;
19
  }
20
  header {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  display: flex;
22
  justify-content: space-between;
23
  align-items: center;
24
- margin-bottom: 30px;
25
- padding-bottom: 15px;
26
- border-bottom: 1px solid #eaeaea;
27
  }
28
- header h1 {
29
- margin: 0;
30
- font-size: 26px;
31
- color: #000;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
  font-weight: 600;
33
  }
 
 
 
 
 
 
34
  .actions button {
35
- padding: 10px 18px;
36
- background-color: #000;
37
- color: #fff;
38
- border: 1px solid #000;
39
  border-radius: 6px;
40
  cursor: pointer;
41
  font-size: 14px;
 
42
  margin-left: 12px;
43
- transition: background-color 0.2s ease, color 0.2s ease;
 
 
 
 
 
 
 
 
 
 
 
 
44
  }
45
- .actions button:hover { background-color: #333; }
46
- .actions button#logout-btn { background-color: #fff; color: #000; border: 1px solid #ccc;}
47
- .actions button#logout-btn:hover { background-color: #f0f0f0; border-color: #bbb;}
48
 
49
  #account-cards-container {
50
  display: grid;
51
- grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
52
- gap: 25px;
53
  }
54
 
55
  .account-card {
56
- padding: 25px;
57
- background-color: #fff;
58
  border-radius: 8px;
59
- border: 1px solid #eaeaea;
60
- box-shadow: 0 4px 15px rgba(0,0,0,0.05);
 
 
 
 
 
61
  }
62
 
63
  .account-info h3 {
64
  margin-top: 0;
65
- font-size: 18px;
66
- color: #000;
67
- border-bottom: 1px solid #eee;
68
- padding-bottom: 10px;
69
- margin-bottom: 15px;
 
 
 
 
 
 
 
70
  font-weight: 500;
71
  }
72
- .account-info p { margin: 8px 0; font-size: 14px; }
73
- .account-info p strong { color: #555; min-width: 160px; display: inline-block; font-weight: 500;}
74
- .status-ok { color: #28a745; font-weight: bold; }
75
- .status-error { color: #dc3545; font-weight: bold; }
 
 
 
76
  .error-details {
77
- font-size: 0.85em;
78
- color: #666;
79
- margin-left: 15px;
80
  white-space: pre-wrap;
81
- background-color:#f9f9f9;
82
- padding: 6px 8px;
83
- border-radius:4px;
84
- margin-top: 3px;
85
- border: 1px solid #eee;
 
 
 
 
 
86
  }
87
  .account-info .card-details-integrated p {
88
  margin: 8px 0;
89
  font-size: 14px;
90
  }
91
  .account-info .card-details-integrated p strong {
92
- min-width: 160px;
93
  }
94
 
95
-
96
  #loading-indicator {
97
  text-align: center;
98
- padding: 40px;
99
  font-size: 16px;
100
- color: #555;
101
  display: none;
102
  width: 100%;
103
  }
104
  #loading-indicator::before {
105
  content: '';
106
  display: inline-block;
107
- width: 24px;
108
- height: 24px;
109
  border: 3px solid rgba(0,0,0, 0.1);
110
  border-radius: 50%;
111
- border-top-color: #333;
112
- animation: spin 1s linear infinite;
113
- margin-right: 12px;
114
  position: relative;
115
- top: 4px;
116
  }
117
  @keyframes spin { to { transform: rotate(360deg); } }
118
-
119
- #last-updated { text-align: right; font-size: 0.85em; color: #777; margin-bottom: 20px; }
120
  </style>
121
  </head>
122
  <body>
123
  <div class="container">
124
  <header>
125
  <h1>账户仪表盘</h1>
 
 
 
 
126
  <div class="actions">
127
  <button id="refresh-btn">刷新数据</button>
128
  <button id="logout-btn">登出</button>
129
  </div>
130
- </header>
131
 
132
- <div id="last-updated">最后更新时间: N/A</div>
133
- <div id="loading-indicator">正在加载数据...</div>
 
 
 
 
 
 
 
 
 
 
 
 
134
 
 
135
  <div id="account-cards-container">
136
  </div>
137
  </div>
@@ -142,7 +256,11 @@
142
  const loadingIndicator = document.getElementById('loading-indicator');
143
  const refreshButton = document.getElementById('refresh-btn');
144
  const logoutButton = document.getElementById('logout-btn');
145
- const lastUpdatedElem = document.getElementById('last-updated');
 
 
 
 
146
 
147
  let autoRefreshInterval;
148
 
@@ -159,13 +277,40 @@
159
  accountCardsContainer.innerHTML = '';
160
  loadingIndicator.style.display = 'none';
161
 
162
- if (Object.keys(data).length === 0) {
 
 
 
 
 
 
163
  accountCardsContainer.innerHTML = '<p style="text-align:center; color: #888; grid-column: 1 / -1;">没有可显示的账户数据。</p>';
 
 
164
  return;
165
  }
166
 
 
 
 
 
 
167
  for (const email in data) {
168
  const account = data[email];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
169
 
170
  const accountCard = document.createElement('div');
171
  accountCard.classList.add('account-card');
@@ -183,34 +328,32 @@
183
  if (account.profile_error) {
184
  contentHTML += `<p class="error-details">Profile 错误: ${account.profile_error}</p>`;
185
  }
 
 
 
 
 
 
186
 
187
  if (account.profile) {
188
  contentHTML += `<p><strong>UID:</strong> ${account.profile.uid || 'N/A'}</p>`;
189
- contentHTML += `<p><strong>可用余额:</strong> $${account.profile.available_balance || '0.00'}</p>`;
190
- contentHTML += `<p><strong>每日消耗:</strong> $${account.profile.daily_consumption || '0.00'}</p>`;
191
  contentHTML += `<p><strong>邀请码:</strong> ${account.profile.invitation_code || 'N/A'}</p>`;
192
- contentHTML += `<p><strong>总收益:</strong> $${account.profile.total_earn_balance || '0.00'}</p>`;
193
  contentHTML += `<p><strong>账户状态:</strong> ${account.profile.status || 'N/A'}</p>`;
194
  } else {
195
  contentHTML += `<p>Profile 数据不可用。</p>`;
196
  }
197
 
198
- contentHTML += `<p><strong>卡片信息获取:</strong> ${formatDateTime(account.last_card_info_attempt)} - ${account.last_card_info_success === true ? '<span class="status-ok">成功</span>' : (account.last_card_info_success === false ? '<span class="status-error">失败</span>' : 'N/A')}</p>`;
199
- if (account.card_info_error) {
200
- contentHTML += `<p class="error-details">卡片错误: ${account.card_info_error}</p>`;
201
- }
202
-
203
  if (account.cards_info) {
204
  const card = account.cards_info;
205
- let cardDisplayName = card.name && card.name.toLowerCase() !== 'n/a' ? card.name : '未命名卡片';
 
 
 
206
 
207
- let cardNumberDisplay = '-';
208
- if (card.provider_bin && card.card_last_four_digits) {
209
- cardNumberDisplay = `${card.provider_bin}xxxxxx${card.card_last_four_digits}`;
210
- } else if (card.provider_bin) {
211
- cardNumberDisplay = `${card.provider_bin}xxxxxx`;
212
- } else if (card.card_last_four_digits) {
213
- cardNumberDisplay = `xxxxxx${card.card_last_four_digits}`;
214
  }
215
 
216
  let dailyLimitDisplay = '-';
@@ -221,22 +364,33 @@
221
  dailyLimitDisplay = `$${card.consumption_limit}`;
222
  }
223
  }
224
- contentHTML += `<div class="card-details-integrated">`;
225
- contentHTML += `<p><strong>卡号:</strong> ${cardNumberDisplay}</p>`;
226
  contentHTML += `<p><strong>卡日限额:</strong> ${dailyLimitDisplay}</p>`;
227
  contentHTML += `</div>`;
228
- } else if (account.last_card_info_success === true) {
229
- contentHTML += `<p><strong>卡信息:</strong> 无可用卡片。</p>`;
 
 
 
 
 
 
 
 
 
 
 
 
230
  }
231
 
232
  contentHTML += `</div>`;
233
  accountCard.innerHTML = contentHTML;
234
  accountCardsContainer.appendChild(accountCard);
235
  }
236
- lastUpdatedElem.textContent = `最后更新时间: ${new Date().toLocaleString('zh-CN', { hour12: false })}`;
 
 
237
  }
238
 
239
-
240
  async function fetchData() {
241
  loadingIndicator.style.display = 'block';
242
  try {
@@ -251,14 +405,12 @@
251
  const data = await response.json();
252
  if (data.error) {
253
  console.error("Error fetching data:", data.error);
254
- tabsContainer.innerHTML = `<p>获取数据时出错: ${data.error}</p>`;
255
  loadingIndicator.style.display = 'none';
256
  } else {
257
  displayData(data);
258
  }
259
  } catch (error) {
260
  console.error('Error fetching data:', error);
261
- tabsContainer.innerHTML = `<p>获取数据时发生网络错误或服务器错误。</p>`;
262
  loadingIndicator.style.display = 'none';
263
  }
264
  }
@@ -303,4 +455,4 @@
303
  });
304
  </script>
305
  </body>
306
- </html>
 
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
  <title>账户仪表盘</title>
7
+ <link rel="preconnect" href="https:
8
+ <link rel="preconnect" href="https:
9
+ <link href="https:
10
  <style>
11
+ :root {
12
+ --bg-color: #ffffff;
13
+ --text-color: #000000;
14
+ --secondary-text-color: #666666;
15
+ --border-color: #eaeaea;
16
+ --card-bg-color: #ffffff;
17
+ --accent-color: #000000;
18
+ --error-color: #ff0000;
19
+ --success-color: #0070f3;
20
+ --summary-bar-bg: #f9f9f9;
21
+ }
22
+
23
  body {
24
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
25
  margin: 0;
26
+ background-color: var(--bg-color);
27
+ color: var(--text-color);
28
  line-height: 1.6;
29
+ -webkit-font-smoothing: antialiased;
30
+ -moz-osx-font-smoothing: grayscale;
31
  }
32
  .container {
33
  max-width: 1200px;
34
+ margin: 0 auto;
35
+ padding: 40px 20px;
36
  }
37
  header {
38
+ display: flex;
39
+ flex-direction: column;
40
+ align-items: flex-start;
41
+ margin-bottom: 20px;
42
+ padding-bottom: 20px;
43
+ border-bottom: 1px solid var(--border-color);
44
+ }
45
+ header h1 {
46
+ margin: 0 0 10px 0;
47
+ font-size: 32px;
48
+ color: var(--text-color);
49
+ font-weight: 700;
50
+ }
51
+
52
+ .top-controls-container {
53
  display: flex;
54
  justify-content: space-between;
55
  align-items: center;
56
+ width: 100%;
57
+ margin-bottom: 20px;
58
+ padding-top: 10px;
59
  }
60
+
61
+ #last-updated {
62
+ font-size: 0.9em;
63
+ color: var(--secondary-text-color);
64
+ }
65
+
66
+ #summary-bar {
67
+ display: flex;
68
+ justify-content: space-between;
69
+ align-items: center;
70
+ background-color: var(--card-bg-color);
71
+ padding: 20px;
72
+ border-radius: 8px;
73
+ margin-bottom: 30px;
74
+ border: 1px solid var(--border-color);
75
+ box-shadow: 0 2px 4px rgba(0,0,0,0.03), 0 1px 2px rgba(0,0,0,0.03);
76
+ }
77
+ .summary-item {
78
+ text-align: center;
79
+ flex-grow: 1;
80
+ padding: 0 10px;
81
+ }
82
+ .summary-item:not(:last-child) {
83
+ border-right: 1px solid var(--border-color);
84
+ }
85
+ .summary-label {
86
+ display: block;
87
+ font-size: 0.9em;
88
+ color: var(--secondary-text-color);
89
+ margin-bottom: 5px;
90
+ font-weight: 500;
91
+ }
92
+ .summary-value {
93
+ display: block;
94
+ font-size: 1.4em;
95
+ color: var(--text-color);
96
  font-weight: 600;
97
  }
98
+
99
+
100
+
101
+ .actions {
102
+ display: flex;
103
+ }
104
  .actions button {
105
+ padding: 10px 20px;
106
+ background-color: var(--accent-color);
107
+ color: var(--bg-color);
108
+ border: 1px solid var(--accent-color);
109
  border-radius: 6px;
110
  cursor: pointer;
111
  font-size: 14px;
112
+ font-weight: 500;
113
  margin-left: 12px;
114
+ transition: opacity 0.2s ease;
115
+ }
116
+ .actions button:hover {
117
+ opacity: 0.8;
118
+ }
119
+ .actions button#logout-btn {
120
+ background-color: var(--bg-color);
121
+ color: var(--accent-color);
122
+ border: 1px solid var(--border-color);
123
+ }
124
+ .actions button#logout-btn:hover {
125
+ background-color: #f9f9f9;
126
+ border-color: #dddddd;
127
  }
 
 
 
128
 
129
  #account-cards-container {
130
  display: grid;
131
+ grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
132
+ gap: 20px;
133
  }
134
 
135
  .account-card {
136
+ padding: 20px;
137
+ background-color: var(--card-bg-color);
138
  border-radius: 8px;
139
+ border: 1px solid var(--border-color);
140
+ box-shadow: 0 2px 4px rgba(0,0,0,0.03), 0 1px 2px rgba(0,0,0,0.03);
141
+ transition: box-shadow 0.2s ease, transform 0.2s ease;
142
+ }
143
+ .account-card:hover {
144
+ box-shadow: 0 5px 15px rgba(0,0,0,0.05), 0 3px 6px rgba(0,0,0,0.05);
145
+ transform: translateY(-2px);
146
  }
147
 
148
  .account-info h3 {
149
  margin-top: 0;
150
+ font-size: 16px;
151
+ color: var(--text-color);
152
+ border-bottom: 1px solid var(--border-color);
153
+ padding-bottom: 12px;
154
+ margin-bottom: 12px;
155
+ font-weight: 600;
156
+ }
157
+ .account-info p { margin: 10px 0; font-size: 14px; color: var(--secondary-text-color); }
158
+ .account-info p strong {
159
+ color: var(--text-color);
160
+ min-width: 130px;
161
+ display: inline-block;
162
  font-weight: 500;
163
  }
164
+ .account-info .highlight-balance {
165
+ font-weight: 700;
166
+ font-size: 1.1em;
167
+ color: var(--text-color);
168
+ }
169
+ .status-ok { color: var(--success-color); font-weight: 500; }
170
+ .status-error { color: var(--error-color); font-weight: 500; }
171
  .error-details {
172
+ font-size: 0.9em;
173
+ color: var(--error-color);
174
+ margin-left: 0;
175
  white-space: pre-wrap;
176
+ background-color: rgba(255,0,0,0.05);
177
+ padding: 8px 10px;
178
+ border-radius: 4px;
179
+ margin-top: 5px;
180
+ border: 1px solid rgba(255,0,0,0.1);
181
+ }
182
+ .account-info .card-details-integrated {
183
+ margin-top: 15px;
184
+ padding-top: 15px;
185
+ border-top: 1px solid var(--border-color);
186
  }
187
  .account-info .card-details-integrated p {
188
  margin: 8px 0;
189
  font-size: 14px;
190
  }
191
  .account-info .card-details-integrated p strong {
192
+ min-width: 100px;
193
  }
194
 
 
195
  #loading-indicator {
196
  text-align: center;
197
+ padding: 50px;
198
  font-size: 16px;
199
+ color: var(--secondary-text-color);
200
  display: none;
201
  width: 100%;
202
  }
203
  #loading-indicator::before {
204
  content: '';
205
  display: inline-block;
206
+ width: 28px;
207
+ height: 28px;
208
  border: 3px solid rgba(0,0,0, 0.1);
209
  border-radius: 50%;
210
+ border-top-color: var(--text-color);
211
+ animation: spin 0.8s linear infinite;
212
+ margin-right: 15px;
213
  position: relative;
214
+ top: 6px;
215
  }
216
  @keyframes spin { to { transform: rotate(360deg); } }
 
 
217
  </style>
218
  </head>
219
  <body>
220
  <div class="container">
221
  <header>
222
  <h1>账户仪表盘</h1>
223
+ </header>
224
+
225
+ <div class="top-controls-container">
226
+ <div id="last-updated">最后更新时间: N/A</div>
227
  <div class="actions">
228
  <button id="refresh-btn">刷新数据</button>
229
  <button id="logout-btn">登出</button>
230
  </div>
231
+ </div>
232
 
233
+ <div id="summary-bar">
234
+ <div class="summary-item">
235
+ <span class="summary-label">总账户数</span>
236
+ <span id="total-accounts-value" class="summary-value">0</span>
237
+ </div>
238
+ <div class="summary-item">
239
+ <span class="summary-label">总可用余额</span>
240
+ <span id="total-balance-value" class="summary-value">$0.00</span>
241
+ </div>
242
+ <div class="summary-item">
243
+ <span class="summary-label">总今日消费</span>
244
+ <span id="total-daily-consumption-value" class="summary-value">$0.00</span>
245
+ </div>
246
+ </div>
247
 
248
+ <div id="loading-indicator">正在加载数据...</div>
249
  <div id="account-cards-container">
250
  </div>
251
  </div>
 
256
  const loadingIndicator = document.getElementById('loading-indicator');
257
  const refreshButton = document.getElementById('refresh-btn');
258
  const logoutButton = document.getElementById('logout-btn');
259
+ const lastUpdatedElem = document.getElementById('last-updated');
260
+ const totalAccountsValueElem = document.getElementById('total-accounts-value');
261
+ const totalBalanceValueElem = document.getElementById('total-balance-value');
262
+ const totalDailyConsumptionValueElem = document.getElementById('total-daily-consumption-value');
263
+
264
 
265
  let autoRefreshInterval;
266
 
 
277
  accountCardsContainer.innerHTML = '';
278
  loadingIndicator.style.display = 'none';
279
 
280
+ const numAccounts = Object.keys(data).length;
281
+ totalAccountsValueElem.textContent = numAccounts;
282
+ const currentTime = new Date().toLocaleString('zh-CN', { hour12: false });
283
+ lastUpdatedElem.textContent = `最后更新时间: ${currentTime}`;
284
+
285
+
286
+ if (numAccounts === 0) {
287
  accountCardsContainer.innerHTML = '<p style="text-align:center; color: #888; grid-column: 1 / -1;">没有可显示的账户数据。</p>';
288
+ totalBalanceValueElem.textContent = '$0.00';
289
+ totalDailyConsumptionValueElem.textContent = '$0.00';
290
  return;
291
  }
292
 
293
+ let totalAvailableBalance = 0;
294
+ let totalDailyConsumption = 0;
295
+ let foundBalance = false;
296
+ let foundConsumption = false;
297
+
298
  for (const email in data) {
299
  const account = data[email];
300
+ if (account.cards_info && account.cards_info.available_balance) {
301
+ const balance = parseFloat(account.cards_info.available_balance);
302
+ if (!isNaN(balance)) {
303
+ totalAvailableBalance += balance;
304
+ foundBalance = true;
305
+ }
306
+ }
307
+ if (account.profile && account.profile.daily_consumption) {
308
+ const consumption = parseFloat(String(account.profile.daily_consumption).replace('$', ''));
309
+ if (!isNaN(consumption)) {
310
+ totalDailyConsumption += consumption;
311
+ foundConsumption = true;
312
+ }
313
+ }
314
 
315
  const accountCard = document.createElement('div');
316
  accountCard.classList.add('account-card');
 
328
  if (account.profile_error) {
329
  contentHTML += `<p class="error-details">Profile 错误: ${account.profile_error}</p>`;
330
  }
331
+
332
+
333
+ contentHTML += `<p><strong>卡片信息获取:</strong> ${formatDateTime(account.last_card_info_attempt)} - ${account.last_card_info_success === true ? '<span class="status-ok">成功</span>' : (account.last_card_info_success === false ? '<span class="status-error">失败</span>' : 'N/A')}</p>`;
334
+ if (account.card_info_error) {
335
+ contentHTML += `<p class="error-details">卡片错误: ${account.card_info_error}</p>`;
336
+ }
337
 
338
  if (account.profile) {
339
  contentHTML += `<p><strong>UID:</strong> ${account.profile.uid || 'N/A'}</p>`;
340
+
 
341
  contentHTML += `<p><strong>邀请码:</strong> ${account.profile.invitation_code || 'N/A'}</p>`;
 
342
  contentHTML += `<p><strong>账户状态:</strong> ${account.profile.status || 'N/A'}</p>`;
343
  } else {
344
  contentHTML += `<p>Profile 数据不可用。</p>`;
345
  }
346
 
 
 
 
 
 
347
  if (account.cards_info) {
348
  const card = account.cards_info;
349
+ contentHTML += `<div class="card-details-integrated">`;
350
+ contentHTML += `<p><strong>卡号:</strong> ${ (card.provider_bin && card.card_last_four_digits) ? `${card.provider_bin}xxxxxx${card.card_last_four_digits}` : (card.provider_bin ? `${card.provider_bin}xxxxxx` : (card.card_last_four_digits ? `xxxxxx${card.card_last_four_digits}` : '-')) }</p>`;
351
+ contentHTML += `<p><strong>可用余额:</strong> <span class="highlight-balance">$${card.available_balance || '0.00'}</span></p>`;
352
+
353
 
354
+ if (account.profile) {
355
+ contentHTML += `<p><strong>每日消耗:</strong> $${account.profile.daily_consumption || '0.00'}</p>`;
356
+ contentHTML += `<p><strong>总收益:</strong> $${account.profile.total_earn_balance || '0.00'}</p>`;
 
 
 
 
357
  }
358
 
359
  let dailyLimitDisplay = '-';
 
364
  dailyLimitDisplay = `$${card.consumption_limit}`;
365
  }
366
  }
 
 
367
  contentHTML += `<p><strong>卡日限额:</strong> ${dailyLimitDisplay}</p>`;
368
  contentHTML += `</div>`;
369
+ } else if (account.last_card_info_success === true) {
370
+ contentHTML += `<div class="card-details-integrated">`;
371
+ if (account.profile) {
372
+ contentHTML += `<p><strong>每日消耗:</strong> $${account.profile.daily_consumption || '0.00'}</p>`;
373
+ contentHTML += `<p><strong>总收益:</strong> $${account.profile.total_earn_balance || '0.00'}</p>`;
374
+ }
375
+ contentHTML += `<p><strong>卡信息:</strong> 无可用卡片。</p></div>`;
376
+ } else {
377
+ if (account.profile) {
378
+ contentHTML += `<div class="card-details-integrated">`;
379
+ contentHTML += `<p><strong>每日消耗:</strong> $${account.profile.daily_consumption || '0.00'}</p>`;
380
+ contentHTML += `<p><strong>总收益:</strong> $${account.profile.total_earn_balance || '0.00'}</p>`;
381
+ contentHTML += `</div>`;
382
+ }
383
  }
384
 
385
  contentHTML += `</div>`;
386
  accountCard.innerHTML = contentHTML;
387
  accountCardsContainer.appendChild(accountCard);
388
  }
389
+
390
+ totalBalanceValueElem.textContent = foundBalance ? `$${totalAvailableBalance.toFixed(2)}` : '$0.00';
391
+ totalDailyConsumptionValueElem.textContent = foundConsumption ? `$${totalDailyConsumption.toFixed(2)}` : '$0.00';
392
  }
393
 
 
394
  async function fetchData() {
395
  loadingIndicator.style.display = 'block';
396
  try {
 
405
  const data = await response.json();
406
  if (data.error) {
407
  console.error("Error fetching data:", data.error);
 
408
  loadingIndicator.style.display = 'none';
409
  } else {
410
  displayData(data);
411
  }
412
  } catch (error) {
413
  console.error('Error fetching data:', error);
 
414
  loadingIndicator.style.display = 'none';
415
  }
416
  }
 
455
  });
456
  </script>
457
  </body>
458
+ </html>