ZHIWEI666 commited on
Commit
7073a8d
·
verified ·
1 Parent(s): 5f3a9b2

大量优化

Browse files
image_moderation.py CHANGED
@@ -198,7 +198,7 @@ async def moderate_image_tencent(image_content: bytes) -> ModerationResult:
198
  response = result.get("Response", {})
199
 
200
  if "Error" in response:
201
- logger.error(f"腾讯云审核API错误: {response['Error']}")
202
  return ModerationResult(passed=True, suggestion="pass",
203
  details={"error": response["Error"]})
204
 
@@ -220,7 +220,7 @@ async def moderate_image_tencent(image_content: bytes) -> ModerationResult:
220
  )
221
 
222
  except Exception as e:
223
- logger.error(f"腾讯云审核异常: {str(e)}")
224
  # 审核服务异常时拒绝,需要人工审核
225
  return ModerationResult(passed=False, suggestion="review",
226
  details={"error": "审核服务暂时不可用,需人工审核"})
 
198
  response = result.get("Response", {})
199
 
200
  if "Error" in response:
201
+ logger.error(f"腾讯云审核API错误: {response.get('Error', 'unknown')!r}")
202
  return ModerationResult(passed=True, suggestion="pass",
203
  details={"error": response["Error"]})
204
 
 
220
  )
221
 
222
  except Exception as e:
223
+ logger.error(f"腾讯云审核异常: {e!r}")
224
  # 审核服务异常时拒绝,需要人工审核
225
  return ModerationResult(passed=False, suggestion="review",
226
  details={"error": "审核服务暂时不可用,需人工审核"})
rate_limiter.py CHANGED
@@ -167,9 +167,18 @@ def record_request(ip: str, endpoint: str):
167
  now = time.time()
168
  key = f"{ip}:{endpoint}"
169
 
170
- # 超出字典上限时触发清理
171
  if len(ip_stats) >= MAX_DICT_SIZE:
172
- cleanup_expired_entries()
 
 
 
 
 
 
 
 
 
173
 
174
  if key not in ip_stats:
175
  ip_stats[key] = {"count": 0, "first_request": now}
 
167
  now = time.time()
168
  key = f"{ip}:{endpoint}"
169
 
170
+ # 🔒 P0安全加固:主动清理机制,防止短期内 OOM
171
  if len(ip_stats) >= MAX_DICT_SIZE:
172
+ # 第一步:清理过期条目(超过1小时未活跃的)
173
+ expired_keys = [k for k, v in list(ip_stats.items()) if now - v.get("last_request", 0) > 3600]
174
+ for k in expired_keys:
175
+ del ip_stats[k]
176
+ # 第二步:如果清理后仍超限,删除最老的 20%
177
+ if len(ip_stats) >= MAX_DICT_SIZE:
178
+ sorted_keys = sorted(ip_stats.keys(), key=lambda k: ip_stats[k].get("last_request", 0))
179
+ for k in sorted_keys[:len(ip_stats) // 5]:
180
+ del ip_stats[k]
181
+ logger.warning(f"IP_STATS_EVICTION | evicted={len(sorted_keys[:len(ip_stats) // 5])}, remaining={len(ip_stats)}")
182
 
183
  if key not in ip_stats:
184
  ip_stats[key] = {"count": 0, "first_request": now}
router_comments.py CHANGED
@@ -13,26 +13,48 @@ router = APIRouter()
13
  @router.post("/api/interactions/toggle")
14
  async def toggle_interaction(interaction: InteractionToggle, current_user: str = Depends(require_auth)):
15
  interaction.user_id = current_user # 强制使用当前用户身份,防止伪造
16
- items_db = db.load_data("items.json", default_data=[])
17
- target_item = next((item for item in items_db if item["id"] == interaction.item_id), None)
18
- if not target_item: raise HTTPException(status_code=404, detail="目标存在问题")
19
-
20
  list_key = f"{interaction.action_type}d_by"
21
  count_key = "likes" if interaction.action_type == "like" else "favorites"
22
- if list_key not in target_item: target_item[list_key] = []; target_item[count_key] = 0
23
-
24
- user_list = target_item[list_key]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  if interaction.is_active:
26
- if interaction.user_id not in user_list:
27
- user_list.append(interaction.user_id)
28
- target_item[count_key] += 1
29
- add_notification(target_item.get("author", ""), {"type": interaction.action_type, "from_user": interaction.user_id, "target_item_id": target_item["id"], "target_item_title": target_item["title"]})
30
- else:
31
- if interaction.user_id in user_list:
32
- user_list.remove(interaction.user_id)
33
- target_item[count_key] = max(0, target_item[count_key] - 1)
34
- db.save_data("items.json", items_db)
35
- return {"status": "success", "new_count": target_item[count_key]}
36
 
37
  @router.post("/api/comments")
38
  async def post_comment(comment: CommentCreate, current_user: str = Depends(require_auth)):
 
13
  @router.post("/api/interactions/toggle")
14
  async def toggle_interaction(interaction: InteractionToggle, current_user: str = Depends(require_auth)):
15
  interaction.user_id = current_user # 强制使用当前用户身份,防止伪造
16
+
 
 
 
17
  list_key = f"{interaction.action_type}d_by"
18
  count_key = "likes" if interaction.action_type == "like" else "favorites"
19
+
20
+ # 使用闭包容器传递原子操作结果
21
+ result_container = {}
22
+
23
+ def updater(items_db):
24
+ target_item = next((item for item in items_db if item["id"] == interaction.item_id), None)
25
+ if not target_item:
26
+ result_container["error"] = "目标存在问题"
27
+ return
28
+
29
+ if list_key not in target_item:
30
+ target_item[list_key] = []
31
+
32
+ user_list = target_item[list_key]
33
+ if interaction.is_active:
34
+ # 双重检查:防止并发重复添加
35
+ if interaction.user_id not in user_list:
36
+ user_list.append(interaction.user_id)
37
+ else:
38
+ if interaction.user_id in user_list:
39
+ user_list.remove(interaction.user_id)
40
+
41
+ # 基于实际列表长度计算 count,即使并发重复也不会导致数量错误
42
+ target_item[count_key] = len(target_item[list_key])
43
+ result_container["count"] = target_item[count_key]
44
+ result_container["author"] = target_item.get("author", "")
45
+ result_container["item_id"] = target_item["id"]
46
+ result_container["title"] = target_item.get("title", "")
47
+
48
+ db.atomic_update("items.json", updater, default_data=[])
49
+
50
+ if result_container.get("error"):
51
+ raise HTTPException(status_code=404, detail=result_container["error"])
52
+
53
+ # 通知在原子操作外发送(通知失败不影响主流程)
54
  if interaction.is_active:
55
+ add_notification(result_container.get("author", ""), {"type": interaction.action_type, "from_user": interaction.user_id, "target_item_id": result_container["item_id"], "target_item_title": result_container["title"]})
56
+
57
+ return {"status": "success", "new_count": result_container["count"]}
 
 
 
 
 
 
 
58
 
59
  @router.post("/api/comments")
60
  async def post_comment(comment: CommentCreate, current_user: str = Depends(require_auth)):
router_items.py CHANGED
@@ -312,7 +312,9 @@ async def get_creator_details(account: str):
312
  }
313
 
314
  @router.post("/api/items")
315
- async def create_item(item: ItemCreate):
 
 
316
  if not item.type:
317
  raise HTTPException(status_code=400, detail="资源类型不能为空")
318
  item.price = int(item.price or 0)
 
312
  }
313
 
314
  @router.post("/api/items")
315
+ async def create_item(item: ItemCreate, current_user: str = Depends(require_auth)):
316
+ # 🔒 P0安全修复:使用 JWT 认证的用户账号作为作者,防止伪造
317
+ item.author = current_user
318
  if not item.type:
319
  raise HTTPException(status_code=400, detail="资源类型不能为空")
320
  item.price = int(item.price or 0)
router_messages.py CHANGED
@@ -152,6 +152,12 @@ async def run_admin_script(req: AdminScriptRequest, current_user: str = Depends(
152
  @router.post("/api/messages/private")
153
  async def send_private_message(msg: PrivateMessage, current_user: str = Depends(require_auth)):
154
  msg.sender = current_user # 强制发送者身份,防止伪造
 
 
 
 
 
 
155
  chats_db = db.load_data("chats.json", default_data={})
156
  conv_id = f"{min(msg.sender, msg.receiver)}_{max(msg.sender, msg.receiver)}"
157
  if conv_id not in chats_db: chats_db[conv_id] = []
 
152
  @router.post("/api/messages/private")
153
  async def send_private_message(msg: PrivateMessage, current_user: str = Depends(require_auth)):
154
  msg.sender = current_user # 强制发送者身份,防止伪造
155
+
156
+ # 验证接收者存在
157
+ users_db = db.load_data("users.json", default_data={})
158
+ if msg.receiver not in users_db:
159
+ raise HTTPException(status_code=404, detail="接收者不存在")
160
+
161
  chats_db = db.load_data("chats.json", default_data={})
162
  conv_id = f"{min(msg.sender, msg.receiver)}_{max(msg.sender, msg.receiver)}"
163
  if conv_id not in chats_db: chats_db[conv_id] = []
router_tasks.py CHANGED
@@ -205,9 +205,14 @@ def check_and_update_expired_tasks(tasks_db, db_session=None):
205
  logger.info(f"TASK_EXPIRED_JSON_SAVED | tasks={len(expired_task_indices)}")
206
 
207
  except Exception as e:
208
- # 💳 P0修复:JSON保存失败,记录warning但不回滚SQL(资金已转移,可后续手动恢复)
209
- logger.warning(f"TASK_EXPIRED_JSON_SAVE_FAILED | error={str(e)} | 资金已退款但任务状态未更新,需手动修复")
210
- # 不抛出异常,因为SQL事务已经成功,资金已经转移
 
 
 
 
 
211
 
212
  # 发送退款通知(在事务外执行,失败不影响主流程)
213
  for item in refund_results:
@@ -319,15 +324,24 @@ async def get_task_detail(task_id: str, current_user: str = None):
319
  publisher_info = user_map.get(task.get("publisher"), {})
320
  assignee_info = user_map.get(task.get("assignee"), {}) if task.get("assignee") else None
321
 
322
- # 构建申请者列表(带用户信息)
323
- applicants_with_info = []
324
- for app in task.get("applicants", []):
325
- app_user = user_map.get(app.get("account"), {})
326
- applicants_with_info.append({
327
- **app,
328
- "name": app_user.get("name", app.get("account")),
329
- "avatar": app_user.get("avatarDataUrl", "")
330
- })
 
 
 
 
 
 
 
 
 
331
 
332
  # 👁️ 过滤敏感字段(详情接口保留 liked_by 和 favorited_by,过滤 viewed_by)
333
  task_data = {k: v for k, v in task.items() if k != "viewed_by"}
@@ -340,7 +354,8 @@ async def get_task_detail(task_id: str, current_user: str = None):
340
  "publisher_avatar": publisher_info.get("avatarDataUrl", ""),
341
  "assignee_name": assignee_info.get("name") if assignee_info else None,
342
  "assignee_avatar": assignee_info.get("avatarDataUrl") if assignee_info else None,
343
- "applicants": applicants_with_info,
 
344
  "status_text": TASK_STATUS.get(task.get("status"), "未知")
345
  }
346
  }
@@ -515,6 +530,10 @@ async def cancel_task(task_id: str, current_user: str = Depends(require_auth), d
515
  取消任务(仅发布者可操作,且仅在 open 状态时可取消)
516
  💳 P0修复:取消时退还冻结资金
517
  """
 
 
 
 
518
  tasks_db = db.load_data("tasks.json", default_data=[])
519
 
520
  for task in tasks_db:
@@ -527,6 +546,11 @@ async def cancel_task(task_id: str, current_user: str = Depends(require_auth), d
527
 
528
  # 💳 P0修复:退还冻结资金
529
  frozen_amount = task.get("frozen_amount", task.get("total_price", 0))
 
 
 
 
 
530
  refund_success = False
531
 
532
  if frozen_amount > 0:
@@ -668,6 +692,10 @@ async def assign_task(task_id: str, assignee: str, current_user: str = Depends(r
668
 
669
  deposit = task.get("deposit_amount", 0)
670
 
 
 
 
 
671
  # 💳 从冻结金额中扣除订金(订金暂时不给接单者,待任务完成后一起给)
672
  wallet = db_session.query(Wallet).filter(Wallet.account == current_user).with_for_update().first()
673
  if not wallet or wallet.frozen_balance < deposit:
@@ -788,6 +816,10 @@ async def accept_task(task_id: str, is_accepted: bool, feedback: str = None, cur
788
  deposit = task.get("deposit_paid", task.get("deposit_amount", 0)) # 优先使用已记录的支付订金
789
  remaining = total_price - deposit # 尾款
790
 
 
 
 
 
791
  if is_accepted:
792
  # 💳 验收通过:支付尾款给接单者
793
  # 💳 P0修复:使用事务保护,失败则回滚
 
205
  logger.info(f"TASK_EXPIRED_JSON_SAVED | tasks={len(expired_task_indices)}")
206
 
207
  except Exception as e:
208
+ # 💳 P0修复:JSON保存失败,记录告警但不回滚SQL(资金已转移,可后续手动恢复)
209
+ import traceback
210
+ traceback.print_exc()
211
+ task_ids = ",".join(item["task_id"] for item in expired_task_indices)
212
+ print(f"[CRITICAL-DATA-INCONSISTENCY] task_id={task_ids}, SQL已退款但JSON状态未更新. Error: {e}")
213
+ logger.critical(f"TASK_EXPIRED_JSON_SAVE_FAILED | [CRITICAL-DATA-INCONSISTENCY] task_ids={task_ids} | SQL已退款但JSON状态未更新 | error={str(e)}")
214
+ # 不抛出异常 - 资金安全退回是更高优先级
215
+ # 定时任务会在下次扫描时修正状态不一致
216
 
217
  # 发送退款通知(在事务外执行,失败不影响主流程)
218
  for item in refund_results:
 
324
  publisher_info = user_map.get(task.get("publisher"), {})
325
  assignee_info = user_map.get(task.get("assignee"), {}) if task.get("assignee") else None
326
 
327
+ # 🔒 P0安全修复:仅任务发布者可查看完整申请者信息,其他人只返回申请人数
328
+ is_publisher = current_user and current_user == task.get("publisher")
329
+ if is_publisher:
330
+ # 发布者可查看完整申请者列表(带用户信息)
331
+ applicants_with_info = []
332
+ for app in task.get("applicants", []):
333
+ app_user = user_map.get(app.get("account"), {})
334
+ applicants_with_info.append({
335
+ **app,
336
+ "name": app_user.get("name", app.get("account")),
337
+ "avatar": app_user.get("avatarDataUrl", "")
338
+ })
339
+ applicants_result = applicants_with_info
340
+ applicant_count = len(task.get("applicants", []))
341
+ else:
342
+ # 非发布者仅返回申请人数,不暴露申请者个人信息
343
+ applicants_result = []
344
+ applicant_count = len(task.get("applicants", []))
345
 
346
  # 👁️ 过滤敏感字段(详情接口保留 liked_by 和 favorited_by,过滤 viewed_by)
347
  task_data = {k: v for k, v in task.items() if k != "viewed_by"}
 
354
  "publisher_avatar": publisher_info.get("avatarDataUrl", ""),
355
  "assignee_name": assignee_info.get("name") if assignee_info else None,
356
  "assignee_avatar": assignee_info.get("avatarDataUrl") if assignee_info else None,
357
+ "applicants": applicants_result,
358
+ "applicant_count": applicant_count,
359
  "status_text": TASK_STATUS.get(task.get("status"), "未知")
360
  }
361
  }
 
530
  取消任务(仅发布者可操作,且仅在 open 状态时可取消)
531
  💳 P0修复:取消时退还冻结资金
532
  """
533
+ # 🔒 P0安全修复:防御性断言,确保 current_user 不为 None
534
+ if not current_user:
535
+ raise HTTPException(status_code=401, detail="未认证")
536
+
537
  tasks_db = db.load_data("tasks.json", default_data=[])
538
 
539
  for task in tasks_db:
 
546
 
547
  # 💳 P0修复:退还冻结资金
548
  frozen_amount = task.get("frozen_amount", task.get("total_price", 0))
549
+
550
+ # 🔒 P0安全修复:冻结金额负数防护
551
+ if frozen_amount < 0:
552
+ raise HTTPException(status_code=400, detail="冻结金额无效,无法退款")
553
+
554
  refund_success = False
555
 
556
  if frozen_amount > 0:
 
692
 
693
  deposit = task.get("deposit_amount", 0)
694
 
695
+ # 🔒 P0安全修复:订金金额负数防护
696
+ if deposit < 0:
697
+ raise HTTPException(status_code=400, detail="订金金额无效")
698
+
699
  # 💳 从冻结金额中扣除订金(订金暂时不给接单者,待任务完成后一起给)
700
  wallet = db_session.query(Wallet).filter(Wallet.account == current_user).with_for_update().first()
701
  if not wallet or wallet.frozen_balance < deposit:
 
816
  deposit = task.get("deposit_paid", task.get("deposit_amount", 0)) # 优先使用已记录的支付订金
817
  remaining = total_price - deposit # 尾款
818
 
819
+ # 🔒 P0安全修复:金额有效性校验
820
+ if total_price < 0 or deposit < 0 or remaining < 0:
821
+ raise HTTPException(status_code=400, detail="任务金额数据异常,无法完成验收")
822
+
823
  if is_accepted:
824
  # 💳 验收通过:支付尾款给接单者
825
  # 💳 P0修复:使用事务保护,失败则回滚
router_users_auth.py CHANGED
@@ -78,8 +78,8 @@ async def send_verify_code(request: Request, req: SendCodeRequest, bg_tasks: Bac
78
  # 🚀 P1性能优化:每次写入前触发清理过期验证码
79
  cleanup_expired_codes()
80
 
81
- # 构建缓存键(联系方式_动作类型)
82
- cache_key = f"{req.contact}_{req.action_type}"
83
 
84
  # 将验证码存入内存缓存,有效期5分钟
85
  VERIFY_CODES[cache_key] = {
@@ -111,7 +111,8 @@ async def send_code_api(req: SendCodeRequest):
111
 
112
  # 生成6位随机验证码
113
  code = str(random.randint(100000, 999999))
114
- key = f"{req.contact}_{req.action_type}"
 
115
 
116
  # 存入缓存,有效期5分钟
117
  VERIFY_CODES[key] = {
@@ -125,7 +126,10 @@ async def send_code_api(req: SendCodeRequest):
125
  send_email_code(req.contact, code, req.action_type)
126
  return {"status": "success", "message": "验证码已成功发送至邮箱"}
127
  except Exception as e:
128
- raise HTTPException(status_code=500, detail=f"邮件发送失败: {str(e)}")
 
 
 
129
 
130
  elif req.contact_type == "phone":
131
  return {"status": "success", "message": "验证码已成功发送至手机"}
@@ -178,7 +182,7 @@ async def register_user(request: Request, user: UserRegister):
178
 
179
  # ========== 第二步:验证码校验(并发安全) ==========
180
  # 根据注册方式构建缓存键
181
- cache_key = f"{user.email}_register" if user.email else f"{user.phone}_register"
182
  # 🔒 P0安全修复:验证码一次性使用,并发安全键防止并发重用
183
  async with _verify_code_lock:
184
  cached = VERIFY_CODES.pop(cache_key, None)
@@ -346,7 +350,7 @@ async def reset_password(request: Request):
346
  raise HTTPException(status_code=400, detail="填写的手机号与该账号绑定的手机号不匹配")
347
 
348
  # 校验验证码(原子性获取+删除)
349
- cache_key = f"{verify_contact}_reset"
350
  # 🔒 P0安全修复:验证码一次性使用,原子性pop防止并发重用
351
  cached = VERIFY_CODES.pop(cache_key, None)
352
  expire_time = cached.get("expires_at", cached.get("expires", 0)) if cached else 0
 
78
  # 🚀 P1性能优化:每次写入前触发清理过期验证码
79
  cleanup_expired_codes()
80
 
81
+ # 🔒 P0安全加固:构建缓存键(联系方式:动作类型),使用冒号分隔符避免与邮箱中的下划线混淆
82
+ cache_key = f"{req.contact}:{req.action_type}"
83
 
84
  # 将验证码存入内存缓存,有效期5分钟
85
  VERIFY_CODES[cache_key] = {
 
111
 
112
  # 生成6位随机验证码
113
  code = str(random.randint(100000, 999999))
114
+ # 🔒 P0安全加固:使用冒号分隔符防止跨操作重用验证码
115
+ key = f"{req.contact}:{req.action_type}"
116
 
117
  # 存入缓存,有效期5分钟
118
  VERIFY_CODES[key] = {
 
126
  send_email_code(req.contact, code, req.action_type)
127
  return {"status": "success", "message": "验证码已成功发送至邮箱"}
128
  except Exception as e:
129
+ # 🔒 P0安全修复:仅服务端记录详细错误,不暴露给前端
130
+ import traceback
131
+ traceback.print_exc()
132
+ raise HTTPException(status_code=500, detail="邮件发送失败,请稍后重试")
133
 
134
  elif req.contact_type == "phone":
135
  return {"status": "success", "message": "验证码已成功发送至手机"}
 
182
 
183
  # ========== 第二步:验证码校验(并发安全) ==========
184
  # 根据注册方式构建缓存键
185
+ cache_key = f"{user.email}:register" if user.email else f"{user.phone}:register"
186
  # 🔒 P0安全修复:验证码一次性使用,并发安全键防止并发重用
187
  async with _verify_code_lock:
188
  cached = VERIFY_CODES.pop(cache_key, None)
 
350
  raise HTTPException(status_code=400, detail="填写的手机号与该账号绑定的手机号不匹配")
351
 
352
  # 校验验证码(原子性获取+删除)
353
+ cache_key = f"{verify_contact}:reset"
354
  # 🔒 P0安全修复:验证码一次性使用,原子性pop防止并发重用
355
  cached = VERIFY_CODES.pop(cache_key, None)
356
  expire_time = cached.get("expires_at", cached.get("expires", 0)) if cached else 0
router_wallet.py CHANGED
@@ -13,7 +13,7 @@
13
  from fastapi import APIRouter, Depends, HTTPException, Request
14
  from fastapi.responses import Response
15
  from sqlalchemy.orm import Session
16
- from pydantic import BaseModel
17
  import time
18
  import uuid
19
  import hashlib
@@ -48,6 +48,9 @@ from verify_code_engine import VERIFY_CODES
48
 
49
  router = APIRouter()
50
 
 
 
 
51
  # ==========================================
52
  # 脱敏工具函数
53
  # ==========================================
@@ -134,6 +137,13 @@ async def create_recharge_order(req: RechargeRequest, current_user: str = Depend
134
  order_id = f"PAY_{uuid.uuid4().hex}"
135
  subject = f"ComfyUI Community Points - {authenticated_account}"
136
 
 
 
 
 
 
 
 
137
  order_string = alipay.api_alipay_trade_precreate(
138
  out_trade_no=order_id,
139
  total_amount=str(req.amount),
@@ -162,50 +172,92 @@ async def alipay_notify(request: Request, db: Session = Depends(get_db)):
162
  if data.get("trade_status") in ("TRADE_SUCCESS", "TRADE_FINISHED"):
163
  order_id = data.get("out_trade_no")
164
 
 
165
  existing_tx = db.query(Transaction).filter(Transaction.tx_id == order_id).first()
166
- if not existing_tx:
167
- amount = int(float(data.get("total_amount", 0)))
168
- account = data.get("subject", "").split(" - ")[-1]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
169
 
170
- # 🔒 P1并发安全:使用悲观锁+异常处理+事务回滚防止并发充值问题
171
- try:
172
- wallet = db.query(Wallet).filter(Wallet.account == account).with_for_update().first()
173
- if not wallet:
174
- wallet = Wallet(account=account, balance=0, earn_balance=0, tip_balance=0, frozen_balance=0)
175
- db.add(wallet)
176
-
177
- wallet.balance = int(wallet.balance or 0) + int(amount)
178
-
179
- last_tx = db.query(Transaction).filter(Transaction.account == account).order_by(Transaction.created_at.desc()).first()
180
- prev_hash = last_tx.tx_hash if last_tx else "GENESIS_HASH"
181
- tx_hash = calculate_tx_hash(order_id, account, "RECHARGE", amount, prev_hash)
182
-
183
- new_tx = Transaction(
184
- tx_id=order_id, account=account, tx_type="RECHARGE", amount=amount,
185
- prev_hash=prev_hash, tx_hash=tx_hash,
186
- description="支付宝充值"
187
- )
188
- db.add(new_tx)
189
- db.commit()
190
-
191
- # 📝 P2优化:充值审计日志
192
- logger.info(f"RECHARGE | account={account} | amount={amount} | order={order_id}")
193
- except Exception as e:
194
- db.rollback()
195
- logger.error(f"RECHARGE_ERROR | account={account} | amount={amount} | order={order_id} | error={str(e)}")
196
- return Response(content="fail", media_type="text/plain")
 
 
 
197
 
198
  return Response(content="success", media_type="text/plain")
199
 
200
  @router.get("/api/wallet/check_order/{order_id}")
201
- async def check_order(order_id: str, account: str = None, current_user: str = Depends(require_auth), db: Session = Depends(get_db)):
 
 
 
 
 
 
 
 
 
 
 
202
  # 防线 1:先查本地数据库(如果 Webhook 成功了,这里就能查到)
203
  tx = db.query(Transaction).filter(Transaction.tx_id == order_id).first()
204
  if tx:
205
  return {"status": "SUCCESS"}
206
 
207
  # 防线 2:主动向支付宝发起查单(解决 Hugging Face 收不到回调的绝杀技)
208
- if alipay and account:
209
  try:
210
  # 拿着订单号去问支付宝:这笔钱到底付了没?
211
  result = alipay.api_alipay_trade_query(out_trade_no=order_id)
@@ -628,14 +680,14 @@ async def withdraw(request: Request, req: WithdrawRequest, current_user: str = D
628
  if req.amount < 1:
629
  raise HTTPException(status_code=400, detail="最小提现金额为1积分")
630
 
631
- # 🔒 验证码缓存键格式:{contact}_{action_type},与 send_code 接口一致
632
  # 先通过账号查询用户邮箱或手机号,再用其构建缓存键(不 fallback 到 account)
633
  users_db = json_db.load_data("users.json", default_data={})
634
  user_info = users_db.get(req.account, {})
635
  user_email = user_info.get("email") or user_info.get("phone", "")
636
  if not user_email:
637
  raise HTTPException(status_code=400, detail="请先绑定邮箱或手机号")
638
- key = f"{user_email}_withdraw"
639
  code_data = VERIFY_CODES.get(key)
640
  # 🔒 P0安全修复:统一使用 expires_at 字段,兼容旧版 expires
641
  expire_time = code_data.get("expires_at", code_data.get("expires", 0)) if code_data else 0
@@ -1191,7 +1243,7 @@ async def get_admin_withdrawals(
1191
  }
1192
 
1193
  class CompleteWithdrawRequest(BaseModel):
1194
- payment_order_id: str
1195
 
1196
  @router.post("/api/admin/withdrawals/{tx_id}/complete")
1197
  async def complete_withdrawal(
 
13
  from fastapi import APIRouter, Depends, HTTPException, Request
14
  from fastapi.responses import Response
15
  from sqlalchemy.orm import Session
16
+ from pydantic import BaseModel, Field
17
  import time
18
  import uuid
19
  import hashlib
 
48
 
49
  router = APIRouter()
50
 
51
+ # 🔒 P0安全修复:充值订单预存映射,回调时通过订单号查找真实账号,不依赖 subject 字段
52
+ PENDING_RECHARGE_ORDERS = {} # {order_id: {"account": str, "amount": int, "created_at": int}}
53
+
54
  # ==========================================
55
  # 脱敏工具函数
56
  # ==========================================
 
137
  order_id = f"PAY_{uuid.uuid4().hex}"
138
  subject = f"ComfyUI Community Points - {authenticated_account}"
139
 
140
+ # 🔒 P0安全修复:预存订单号->账号映射,供回调时安全查找
141
+ PENDING_RECHARGE_ORDERS[order_id] = {
142
+ "account": authenticated_account,
143
+ "amount": req.amount,
144
+ "created_at": int(time.time())
145
+ }
146
+
147
  order_string = alipay.api_alipay_trade_precreate(
148
  out_trade_no=order_id,
149
  total_amount=str(req.amount),
 
172
  if data.get("trade_status") in ("TRADE_SUCCESS", "TRADE_FINISHED"):
173
  order_id = data.get("out_trade_no")
174
 
175
+ # 🔒 第一层防御:快速预检查(优化,避免不必要的锁获取)
176
  existing_tx = db.query(Transaction).filter(Transaction.tx_id == order_id).first()
177
+ if existing_tx:
178
+ return Response(content="success", media_type="text/plain")
179
+
180
+ amount = int(float(data.get("total_amount", 0)))
181
+
182
+ # 🔒 P0安全修复:通过预存订单映射查找真实账号,不依赖 subject 字段提取
183
+ order_info = PENDING_RECHARGE_ORDERS.get(order_id)
184
+ if not order_info:
185
+ logger.warning(f"RECHARGE_ORDER_NOT_FOUND | order={order_id} | subject={data.get('subject', '')}")
186
+ return Response(content="fail", media_type="text/plain")
187
+ account = order_info["account"]
188
+
189
+ # 🔒 纵深防御:验证 subject 中的账号与预存映射一致
190
+ subject_account = data.get("subject", "").split(" - ")[-1] if " - " in data.get("subject", "") else ""
191
+ if subject_account and subject_account != account:
192
+ logger.warning(f"RECHARGE_ACCOUNT_MISMATCH | order={order_id} | expected={account} | subject={subject_account}")
193
+ return Response(content="fail", media_type="text/plain")
194
+
195
+ # 清理已处理的预存订单
196
+ PENDING_RECHARGE_ORDERS.pop(order_id, None)
197
+
198
+ # 🔒 P0安全修复:充值金额有效性校验
199
+ if amount <= 0:
200
+ logger.warning(f"RECHARGE_AMOUNT_INVALID | order={order_id} | amount={amount}")
201
+ return Response(content="fail", media_type="text/plain")
202
+
203
+ # 🔒 P1并发安全:使用悲观锁+异常处理+事务回滚防止并发充值问题
204
+ # 🔒 P1 TOCTOU修复:先获取行锁,再在锁内检查重复,确保检查和操作的原子性
205
+ try:
206
+ wallet = db.query(Wallet).filter(Wallet.account == account).with_for_update().first()
207
 
208
+ # 🔒 第二层防御:在持有行锁后再检查是否已处理过此订单(原子性防重复充值)
209
+ existing_tx = db.query(Transaction).filter(Transaction.tx_id == order_id).first()
210
+ if existing_tx:
211
+ db.commit() # 释放锁
212
+ return Response(content="success", media_type="text/plain") # 幂等返回成功
213
+
214
+ if not wallet:
215
+ wallet = Wallet(account=account, balance=0, earn_balance=0, tip_balance=0, frozen_balance=0)
216
+ db.add(wallet)
217
+
218
+ wallet.balance = int(wallet.balance or 0) + int(amount)
219
+
220
+ last_tx = db.query(Transaction).filter(Transaction.account == account).order_by(Transaction.created_at.desc()).first()
221
+ prev_hash = last_tx.tx_hash if last_tx else "GENESIS_HASH"
222
+ tx_hash = calculate_tx_hash(order_id, account, "RECHARGE", amount, prev_hash)
223
+
224
+ new_tx = Transaction(
225
+ tx_id=order_id, account=account, tx_type="RECHARGE", amount=amount,
226
+ prev_hash=prev_hash, tx_hash=tx_hash,
227
+ description="支付宝充值"
228
+ )
229
+ db.add(new_tx)
230
+ db.commit()
231
+
232
+ # 📝 P2优化:充值审计日志
233
+ logger.info(f"RECHARGE | account={account} | amount={amount} | order={order_id}")
234
+ except Exception as e:
235
+ db.rollback()
236
+ logger.error(f"RECHARGE_ERROR | account={account} | amount={amount} | order={order_id} | error={str(e)}")
237
+ return Response(content="fail", media_type="text/plain")
238
 
239
  return Response(content="success", media_type="text/plain")
240
 
241
  @router.get("/api/wallet/check_order/{order_id}")
242
+ async def check_order(order_id: str, current_user: str = Depends(require_auth), db: Session = Depends(get_db)):
243
+ # 🔒 P0安全修复:使用认证用户账号,不再接受客户端传入的 account 参数
244
+ account = current_user
245
+
246
+ # 🔒 纵深防御:优先从预存订单映射查找账号
247
+ order_info = PENDING_RECHARGE_ORDERS.get(order_id)
248
+ if order_info:
249
+ account = order_info["account"]
250
+ # 验证认证用户与订单所属用户一致
251
+ if current_user != account:
252
+ logger.warning(f"CHECK_ORDER_ACCOUNT_MISMATCH | order={order_id} | auth={current_user} | order_account={account}")
253
+ return {"status": "PENDING"}
254
  # 防线 1:先查本地数据库(如果 Webhook 成功了,这里就能查到)
255
  tx = db.query(Transaction).filter(Transaction.tx_id == order_id).first()
256
  if tx:
257
  return {"status": "SUCCESS"}
258
 
259
  # 防线 2:主动向支付宝发起查单(解决 Hugging Face 收不到回调的绝杀技)
260
+ if alipay:
261
  try:
262
  # 拿着订单号去问支付宝:这笔钱到底付了没?
263
  result = alipay.api_alipay_trade_query(out_trade_no=order_id)
 
680
  if req.amount < 1:
681
  raise HTTPException(status_code=400, detail="最小提现金额为1积分")
682
 
683
+ # 🔒 验证码缓存键格式:{contact}:{action_type},与 send_code 接口一致
684
  # 先通过账号查询用户邮箱或手机号,再用其构建缓存键(不 fallback 到 account)
685
  users_db = json_db.load_data("users.json", default_data={})
686
  user_info = users_db.get(req.account, {})
687
  user_email = user_info.get("email") or user_info.get("phone", "")
688
  if not user_email:
689
  raise HTTPException(status_code=400, detail="请先绑定邮箱或手机号")
690
+ key = f"{user_email}:withdraw"
691
  code_data = VERIFY_CODES.get(key)
692
  # 🔒 P0安全修复:统一使用 expires_at 字段,兼容旧版 expires
693
  expire_time = code_data.get("expires_at", code_data.get("expires", 0)) if code_data else 0
 
1243
  }
1244
 
1245
  class CompleteWithdrawRequest(BaseModel):
1246
+ payment_order_id: str = Field(..., min_length=1, max_length=64, description="支付宝/银行打款流水号")
1247
 
1248
  @router.post("/api/admin/withdrawals/{tx_id}/complete")
1249
  async def complete_withdrawal(
数据库连接.py CHANGED
@@ -71,7 +71,7 @@ os.makedirs(BACKUP_DIR, exist_ok=True)
71
  CACHE_TTL = {
72
  "items.json": 120, # 列表数据 2 分钟
73
  "tasks.json": 60, # 任务数据 1 分钟
74
- "users.json": 300, # 用户数据 5 分钟
75
  "posts.json": 120, # 帖子数据 2 分钟
76
  "comments.json": 60, # 评论数据 1 分钟
77
  "disputes.json": 60, # 申诉数据 1 分钟
 
71
  CACHE_TTL = {
72
  "items.json": 120, # 列表数据 2 分钟
73
  "tasks.json": 60, # 任务数据 1 分钟
74
+ "users.json": 30, # 🔒 用户数据 30秒(含权限字段 is_admin,缩短 TTL 确保权限变更及时生效)
75
  "posts.json": 120, # 帖子数据 2 分钟
76
  "comments.json": 60, # 评论数据 1 分钟
77
  "disputes.json": 60, # 申诉数据 1 分钟