ZHIWEI666 commited on
Commit
a756543
·
verified ·
1 Parent(s): 8689a84

Upload 26 files

Browse files
Files changed (3) hide show
  1. router_posts.py +144 -28
  2. router_tasks.py +285 -91
  3. router_wallet.py +53 -22
router_posts.py CHANGED
@@ -6,12 +6,19 @@
6
  # ==========================================
7
 
8
  from fastapi import APIRouter, HTTPException, Depends
 
9
  from models import PostCreate, PostUpdate
10
  import 数据库连接 as db
11
  from 安全认证 import require_auth
12
  from db_utils import record_view, sort_cache
 
 
 
13
  import time
14
  import uuid
 
 
 
15
 
16
  router = APIRouter()
17
 
@@ -313,15 +320,23 @@ async def toggle_favorite(post_id: str, current_user: str = Depends(require_auth
313
  # 🎁 打赏接口
314
  # ==========================================
315
 
 
 
 
 
 
 
 
316
  @router.post("/api/posts/{post_id}/tip")
317
- async def tip_post(post_id: str, amount: int, is_anon: bool = False, current_user: str = Depends(require_auth)):
318
  """
319
- 打赏帖子(原子操作,并发安全)
320
  """
321
  if amount <= 0:
322
  raise HTTPException(status_code=400, detail="打赏金额必须大于0")
323
 
324
  result_container = [None]
 
325
 
326
  def updater(data):
327
  # 在锁内查找帖子
@@ -340,22 +355,7 @@ async def tip_post(post_id: str, amount: int, is_anon: bool = False, current_use
340
  result_container[0] = {"error": "self_tip"}
341
  return
342
 
343
- # 在锁内操作用户余额
344
- users_db = db.load_data("users.json", default_data={})
345
- tipper = users_db.get(current_user)
346
- if not tipper or tipper.get("balance", 0) < amount:
347
- result_container[0] = {"error": "insufficient_balance"}
348
- return
349
-
350
- author_account = target_post.get("author")
351
- author = users_db.get(author_account)
352
- if not author:
353
- result_container[0] = {"error": "author_not_found"}
354
- return
355
-
356
- # 扣款加款
357
- tipper["balance"] -= amount
358
- author["balance"] += amount
359
 
360
  # 更新打赏榜单
361
  tip_board = target_post.get("tip_board", [])
@@ -367,25 +367,141 @@ async def tip_post(post_id: str, amount: int, is_anon: bool = False, current_use
367
  tip_board.sort(key=lambda x: x["amount"], reverse=True)
368
  target_post["tip_board"] = tip_board
369
 
370
- # 保存用户数据
371
- db.save_data("users.json", users_db)
372
-
373
  result_container[0] = {"status": "success", "message": f"成功打赏 {amount} 积分"}
374
 
375
  db.atomic_update("posts.json", updater, default_data=[])
376
 
377
- # 🗂️ 清除排序缓存(打赏可能影响排序)
378
- sort_cache.invalidate("posts:")
379
-
380
  result = result_container[0]
381
  if result is None or result.get("error") == "not_found":
382
  raise HTTPException(status_code=404, detail="帖子不存在")
383
  if result.get("error") == "self_tip":
384
  raise HTTPException(status_code=400, detail="不能打赏自己的帖子")
385
- if result.get("error") == "insufficient_balance":
386
- raise HTTPException(status_code=400, detail="余额不足")
387
- if result.get("error") == "author_not_found":
388
- raise HTTPException(status_code=404, detail="作者账户不存在")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
389
 
390
  return result
391
 
 
6
  # ==========================================
7
 
8
  from fastapi import APIRouter, HTTPException, Depends
9
+ from sqlalchemy.orm import Session
10
  from models import PostCreate, PostUpdate
11
  import 数据库连接 as db
12
  from 安全认证 import require_auth
13
  from db_utils import record_view, sort_cache
14
+ from database_sql import get_db
15
+ from models_sql import Wallet, Transaction
16
+ from notifications import add_notification
17
  import time
18
  import uuid
19
+ import hashlib
20
+ import datetime
21
+ import logging
22
 
23
  router = APIRouter()
24
 
 
320
  # 🎁 打赏接口
321
  # ==========================================
322
 
323
+ # 📝 审计日志
324
+ logger = logging.getLogger("ComfyUI-Ranking.Posts")
325
+
326
+ def calculate_tx_hash(tx_id, account, tx_type, amount, prev_hash):
327
+ data = f"{tx_id}{account}{tx_type}{amount}{prev_hash}"
328
+ return hashlib.sha256(data.encode()).hexdigest()
329
+
330
  @router.post("/api/posts/{post_id}/tip")
331
+ async def tip_post(post_id: str, amount: int, is_anon: bool = False, current_user: str = Depends(require_auth), db_session: Session = Depends(get_db)):
332
  """
333
+ 打赏帖子(原子操作,并发安全)- 使用 SQL Wallet 系统
334
  """
335
  if amount <= 0:
336
  raise HTTPException(status_code=400, detail="打赏金额必须大于0")
337
 
338
  result_container = [None]
339
+ author_account = [None] # 用于在原子操作外获取作者账号
340
 
341
  def updater(data):
342
  # 在锁内查找帖子
 
355
  result_container[0] = {"error": "self_tip"}
356
  return
357
 
358
+ author_account[0] = target_post.get("author")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
359
 
360
  # 更新打赏榜单
361
  tip_board = target_post.get("tip_board", [])
 
367
  tip_board.sort(key=lambda x: x["amount"], reverse=True)
368
  target_post["tip_board"] = tip_board
369
 
 
 
 
370
  result_container[0] = {"status": "success", "message": f"成功打赏 {amount} 积分"}
371
 
372
  db.atomic_update("posts.json", updater, default_data=[])
373
 
 
 
 
374
  result = result_container[0]
375
  if result is None or result.get("error") == "not_found":
376
  raise HTTPException(status_code=404, detail="帖子不存在")
377
  if result.get("error") == "self_tip":
378
  raise HTTPException(status_code=400, detail="不能打赏自己的帖子")
379
+
380
+ # 💳 使用 SQL Wallet 系统处理余额转账
381
+ try:
382
+ author = author_account[0]
383
+ if not author:
384
+ raise HTTPException(status_code=404, detail="作者账户不存在")
385
+
386
+ # 🔒 P1幂等性防护:检查最近5秒内是否存在相同交易
387
+ recent_cutoff = datetime.datetime.utcnow() - datetime.timedelta(seconds=5)
388
+ duplicate_tx = db_session.query(Transaction).filter(
389
+ Transaction.account == current_user,
390
+ Transaction.tx_type == "TIP_OUT",
391
+ Transaction.amount == -amount,
392
+ Transaction.related_account == author,
393
+ Transaction.created_at >= recent_cutoff
394
+ ).first()
395
+ if duplicate_tx:
396
+ return {"status": "success", "message": "打赏已处理(重复请求)"}
397
+
398
+ # 🔒 并发安全:使用悲观锁获取双方钱包
399
+ tipper_wallet = db_session.query(Wallet).filter(Wallet.account == current_user).with_for_update().first()
400
+ author_wallet = db_session.query(Wallet).filter(Wallet.account == author).with_for_update().first()
401
+
402
+ if not tipper_wallet or tipper_wallet.balance < amount:
403
+ raise HTTPException(status_code=400, detail="余额不足")
404
+ if not author_wallet:
405
+ # 如果作者钱包不存在,创建一个
406
+ author_wallet = Wallet(account=author, balance=0, earn_balance=0, tip_balance=0, frozen_balance=0)
407
+ db_session.add(author_wallet)
408
+
409
+ # 执行转账
410
+ tipper_wallet.balance -= amount
411
+ author_wallet.balance += amount # 实际收入进统一余额
412
+ author_wallet.tip_balance += amount # 累计打赏收益统计(只增不减)
413
+
414
+ # 创建交易记录
415
+ tx_id_tipper = f"TIP_OUT_{int(time.time())}_{uuid.uuid4().hex[:6]}"
416
+ tx_id_author = f"TIP_IN_{int(time.time())}_{uuid.uuid4().hex[:6]}"
417
+
418
+ # 获取最后交易记录的哈希
419
+ last_tx_tipper = db_session.query(Transaction).filter(Transaction.account == current_user).order_by(Transaction.created_at.desc()).first()
420
+ last_tx_author = db_session.query(Transaction).filter(Transaction.account == author).order_by(Transaction.created_at.desc()).first()
421
+ prev_hash_tipper = last_tx_tipper.tx_hash if last_tx_tipper else "GENESIS_HASH"
422
+ prev_hash_author = last_tx_author.tx_hash if last_tx_author else "GENESIS_HASH"
423
+
424
+ # 获取用户信息和帖子标题
425
+ users_db = db.load_data("users.json", default_data={})
426
+ posts_db = db.load_data("posts.json", default_data=[])
427
+ author_info = users_db.get(author, {})
428
+ tipper_info = users_db.get(current_user, {})
429
+ author_name = author_info.get("name", author)
430
+ tipper_name = tipper_info.get("name", current_user)
431
+
432
+ post_title = None
433
+ for post in posts_db:
434
+ if post["id"] == post_id:
435
+ post_title = post.get("title")
436
+ break
437
+
438
+ # 打赏方交易记录 (TIP_OUT)
439
+ tx_tipper = Transaction(
440
+ tx_id=tx_id_tipper,
441
+ account=current_user,
442
+ tx_type="TIP_OUT",
443
+ amount=-amount,
444
+ related_account=author,
445
+ item_id=post_id,
446
+ prev_hash=prev_hash_tipper,
447
+ tx_hash=calculate_tx_hash(tx_id_tipper, current_user, "TIP_OUT", -amount, prev_hash_tipper),
448
+ description=f"打赏给 {author_name}" + (f" 的帖子《{post_title}》" if post_title else ""),
449
+ item_title=post_title,
450
+ item_type="post",
451
+ related_user_name=author_name
452
+ )
453
+
454
+ # 接收方交易记录 (TIP_IN)
455
+ tx_author = Transaction(
456
+ tx_id=tx_id_author,
457
+ account=author,
458
+ tx_type="TIP_IN",
459
+ amount=amount,
460
+ related_account=current_user,
461
+ item_id=post_id,
462
+ prev_hash=prev_hash_author,
463
+ tx_hash=calculate_tx_hash(tx_id_author, author, "TIP_IN", amount, prev_hash_author),
464
+ description=f"收到 {tipper_name} 的帖子打赏" + (f" ({post_title})" if post_title else ""),
465
+ item_title=post_title,
466
+ item_type="post",
467
+ related_user_name=tipper_name if not is_anon else "匿名用户"
468
+ )
469
+
470
+ db_session.add(tx_tipper)
471
+ db_session.add(tx_author)
472
+ db_session.commit()
473
+
474
+ # 📝 审计日志
475
+ logger.info(f"POST_TIP | from={current_user} | to={author} | amount={amount} | post={post_id} | anon={is_anon}")
476
+
477
+ # 🔔 打赏通知(考虑匿名)
478
+ if not is_anon:
479
+ add_notification(author, {
480
+ "type": "tip",
481
+ "from_user": current_user,
482
+ "target_item_id": post_id,
483
+ "target_item_title": post_title or "",
484
+ "content": f"您收到来自 {tipper_name} 的 {amount} 积分帖子打赏"
485
+ })
486
+ else:
487
+ add_notification(author, {
488
+ "type": "tip",
489
+ "from_user": "anonymous",
490
+ "target_item_id": post_id,
491
+ "target_item_title": "",
492
+ "content": f"您收到了一份 {amount} 积分的匿名帖子打赏"
493
+ })
494
+
495
+ # 🗂️ 清除排序缓存(打赏可能影响排序)
496
+ sort_cache.invalidate("posts:")
497
+
498
+ except HTTPException:
499
+ db_session.rollback()
500
+ raise
501
+ except Exception as e:
502
+ db_session.rollback()
503
+ logger.error(f"POST_TIP_ERROR | from={current_user} | post={post_id} | amount={amount} | error={str(e)}")
504
+ raise HTTPException(status_code=500, detail="打赏处理失败,请稍后重试")
505
 
506
  return result
507
 
router_tasks.py CHANGED
@@ -806,6 +806,7 @@ async def accept_task(task_id: str, is_accepted: bool, feedback: str = None, cur
806
  db_session.flush()
807
 
808
  assignee_wallet.balance += total_price # 全款进入可用余额
 
809
 
810
  # 获取双方用户名
811
  users_db = db.load_data("users.json", default_data={})
@@ -1073,12 +1074,13 @@ async def get_admin_disputes(status: str = None, current_user: str = Depends(req
1073
  return {"status": "success", "data": result}
1074
 
1075
  @router.post("/api/admin/disputes/{dispute_id}/resolve")
1076
- async def resolve_dispute(dispute_id: str, resolution: str, ratio: int = None, note: str = None, current_user: str = Depends(require_auth)):
1077
  """
1078
  管理员裁决申诉
1079
  - resolution: favor_initiator(支持申诉方) / favor_respondent(支持被申诉方) / split(按比例分成)
1080
  - ratio: 分成比例(0-100),表示申诉方获得的比例。仅split时有效
1081
  - note: 裁决说明
 
1082
  """
1083
  if not is_admin(current_user):
1084
  raise HTTPException(status_code=403, detail="需要管理员权限")
@@ -1106,75 +1108,164 @@ async def resolve_dispute(dispute_id: str, resolution: str, ratio: int = None, n
1106
  total_price = task.get("total_price", 0)
1107
  deposit = task.get("deposit_amount", 0)
1108
 
1109
- publisher = users_db.get(dispute.get("publisher"))
1110
- assignee = users_db.get(dispute.get("assignee"))
1111
 
1112
  is_publisher_initiator = dispute.get("initiator_role") == "publisher"
1113
- initiator = publisher if is_publisher_initiator else assignee
1114
- respondent = assignee if is_publisher_initiator else publisher
1115
-
1116
- # 计算资金分配
1117
- if resolution == "favor_initiator":
1118
- # 支持申诉方
1119
- if is_publisher_initiator:
1120
- # 发布者胜诉:退还订金
1121
- if publisher:
1122
- publisher["balance"] = publisher.get("balance", 0) + deposit
1123
- initiator_amount = deposit
1124
- respondent_amount = 0
1125
- else:
1126
- # 接单者胜诉:获得全款
1127
- if publisher:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1128
  remaining = total_price - deposit
1129
- publisher["balance"] = publisher.get("balance", 0) - remaining
1130
- if assignee:
1131
- assignee["balance"] = assignee.get("balance", 0) + total_price
1132
- initiator_amount = total_price
1133
- respondent_amount = 0
1134
- elif resolution == "favor_respondent":
1135
- # 支持被申诉方
1136
- if is_publisher_initiator:
1137
- # 接单者胜诉:获得全款
1138
- if publisher:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1139
  remaining = total_price - deposit
1140
- publisher["balance"] = publisher.get("balance", 0) - remaining
1141
- if assignee:
1142
- assignee["balance"] = assignee.get("balance", 0) + total_price
1143
- initiator_amount = 0
1144
- respondent_amount = total_price
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1145
  else:
1146
- # 发布者胜诉:退还订金
1147
- if publisher:
1148
- publisher["balance"] = publisher.get("balance", 0) + deposit
1149
- initiator_amount = 0
1150
- respondent_amount = deposit
1151
- else:
1152
- # 按比例分成
1153
- initiator_share = int(total_price * ratio / 100)
1154
- respondent_share = total_price - initiator_share
1155
-
1156
- # 处理资金:发布者支付尾款,然后按比例分配
1157
- if publisher:
1158
  remaining = total_price - deposit
1159
- publisher["balance"] = publisher.get("balance", 0) - remaining
1160
-
1161
- # 发布者获得退款部分,接单者获得报酬部分
1162
- if is_publisher_initiator:
1163
- # 申诉方是发布者
1164
- pub_refund = initiator_share
1165
- assignee_earn = respondent_share
1166
- else:
1167
- # 申诉方是接单者
1168
- assignee_earn = initiator_share
1169
- pub_refund = respondent_share
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1170
 
1171
- if publisher:
1172
- publisher["balance"] = publisher.get("balance", 0) + pub_refund
1173
- if assignee:
1174
- assignee["balance"] = assignee.get("balance", 0) + assignee_earn
1175
 
1176
- initiator_amount = initiator_share
1177
- respondent_amount = respondent_share
 
 
1178
 
1179
  # 更新申诉状态
1180
  dispute["resolution"] = resolution
@@ -1191,7 +1282,6 @@ async def resolve_dispute(dispute_id: str, resolution: str, ratio: int = None, n
1191
 
1192
  db.save_data("disputes.json", disputes_db)
1193
  db.save_data("tasks.json", tasks_db)
1194
- db.save_data("users.json", users_db)
1195
  # 🗂️ 清除排序缓存(任务状态变为已完成)
1196
  sort_cache.invalidate("tasks:")
1197
 
@@ -1221,6 +1311,8 @@ async def resolve_dispute(dispute_id: str, resolution: str, ratio: int = None, n
1221
  "content": f"任务申诉已裁决:{resolution_text},您获得{respondent_amount}积分"
1222
  })
1223
 
 
 
1224
  return {"status": "success", "message": f"裁决完成:{resolution_text}"}
1225
 
1226
  raise HTTPException(status_code=404, detail="申诉不存在")
@@ -1348,14 +1440,15 @@ async def toggle_favorite(task_id: str, current_user: str = Depends(require_auth
1348
  # ==========================================
1349
 
1350
  @router.post("/api/tasks/{task_id}/tip")
1351
- async def tip_task(task_id: str, amount: int, is_anon: bool = False, current_user: str = Depends(require_auth)):
1352
  """
1353
- 打赏任务(原子操作,并发安全)
1354
  """
1355
  if amount <= 0:
1356
  raise HTTPException(status_code=400, detail="打赏金额必须大于0")
1357
 
1358
  result_container = [None]
 
1359
 
1360
  def updater(data):
1361
  # 在锁内查找任务
@@ -1374,22 +1467,7 @@ async def tip_task(task_id: str, amount: int, is_anon: bool = False, current_use
1374
  result_container[0] = {"error": "self_tip"}
1375
  return
1376
 
1377
- # 在锁内操作用户余额
1378
- users_db = db.load_data("users.json", default_data={})
1379
- tipper = users_db.get(current_user)
1380
- if not tipper or tipper.get("balance", 0) < amount:
1381
- result_container[0] = {"error": "insufficient_balance"}
1382
- return
1383
-
1384
- publisher = target_task.get("publisher")
1385
- author = users_db.get(publisher)
1386
- if not author:
1387
- result_container[0] = {"error": "author_not_found"}
1388
- return
1389
-
1390
- # 扣款加款
1391
- tipper["balance"] -= amount
1392
- author["balance"] += amount
1393
 
1394
  # 更新打赏榜单
1395
  tip_board = target_task.get("tip_board", [])
@@ -1401,25 +1479,141 @@ async def tip_task(task_id: str, amount: int, is_anon: bool = False, current_use
1401
  tip_board.sort(key=lambda x: x["amount"], reverse=True)
1402
  target_task["tip_board"] = tip_board
1403
 
1404
- # 保存用户数据
1405
- db.save_data("users.json", users_db)
1406
-
1407
  result_container[0] = {"status": "success", "message": f"成功打赏 {amount} 积分"}
1408
 
1409
  db.atomic_update("tasks.json", updater, default_data=[])
1410
 
1411
- # 🗂️ 清除排序缓存(打赏可能影响排序)
1412
- sort_cache.invalidate("tasks:")
1413
-
1414
  result = result_container[0]
1415
  if result is None or result.get("error") == "not_found":
1416
  raise HTTPException(status_code=404, detail="任务不存在")
1417
  if result.get("error") == "self_tip":
1418
  raise HTTPException(status_code=400, detail="不能打赏自己的任务")
1419
- if result.get("error") == "insufficient_balance":
1420
- raise HTTPException(status_code=400, detail="余额不足")
1421
- if result.get("error") == "author_not_found":
1422
- raise HTTPException(status_code=404, detail="作者账户不存在")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1423
 
1424
  return result
1425
 
 
806
  db_session.flush()
807
 
808
  assignee_wallet.balance += total_price # 全款进入可用余额
809
+ assignee_wallet.earn_balance += total_price # 累计任务收益统计(只增不减)
810
 
811
  # 获取双方用户名
812
  users_db = db.load_data("users.json", default_data={})
 
1074
  return {"status": "success", "data": result}
1075
 
1076
  @router.post("/api/admin/disputes/{dispute_id}/resolve")
1077
+ async def resolve_dispute(dispute_id: str, resolution: str, ratio: int = None, note: str = None, current_user: str = Depends(require_auth), db_session: Session = Depends(get_db)):
1078
  """
1079
  管理员裁决申诉
1080
  - resolution: favor_initiator(支持申诉方) / favor_respondent(支持被申诉方) / split(按比例分成)
1081
  - ratio: 分成比例(0-100),表示申诉方获得的比例。仅split时有效
1082
  - note: 裁决说明
1083
+ 💳 P0改造:使用 SQL Wallet 系统处理资金,替代 JSON balance 操作
1084
  """
1085
  if not is_admin(current_user):
1086
  raise HTTPException(status_code=403, detail="需要管理员权限")
 
1108
  total_price = task.get("total_price", 0)
1109
  deposit = task.get("deposit_amount", 0)
1110
 
1111
+ publisher_account = dispute.get("publisher")
1112
+ assignee_account = dispute.get("assignee")
1113
 
1114
  is_publisher_initiator = dispute.get("initiator_role") == "publisher"
1115
+
1116
+ # 💳 P0改造:使用 SQL Wallet 处理资金
1117
+ try:
1118
+ # 获取双方钱包(带悲观锁)
1119
+ publisher_wallet = db_session.query(Wallet).filter(Wallet.account == publisher_account).with_for_update().first()
1120
+ assignee_wallet = db_session.query(Wallet).filter(Wallet.account == assignee_account).with_for_update().first()
1121
+
1122
+ # 如果钱包不存在,创建新钱包
1123
+ if not publisher_wallet:
1124
+ publisher_wallet = Wallet(account=publisher_account, balance=0)
1125
+ db_session.add(publisher_wallet)
1126
+ db_session.flush()
1127
+ if not assignee_wallet:
1128
+ assignee_wallet = Wallet(account=assignee_account, balance=0)
1129
+ db_session.add(assignee_wallet)
1130
+ db_session.flush()
1131
+
1132
+ # 获取双方用户名
1133
+ publisher_info = users_db.get(publisher_account, {})
1134
+ assignee_info = users_db.get(assignee_account, {})
1135
+ publisher_name = publisher_info.get("name", publisher_account)
1136
+ assignee_name = assignee_info.get("name", assignee_account)
1137
+
1138
+ # 计算资金分配
1139
+ if resolution == "favor_initiator":
1140
+ # 支持申诉方
1141
+ if is_publisher_initiator:
1142
+ # 发布者胜诉:退还订金(从冻结余额释放到可用余额)
1143
+ publisher_wallet.frozen_balance = max(0, publisher_wallet.frozen_balance - deposit)
1144
+ publisher_wallet.balance += deposit
1145
+
1146
+ # 记录交易
1147
+ create_task_transaction(
1148
+ db_session, publisher_account, "TASK_REFUND",
1149
+ deposit, task_id=task.get("id"),
1150
+ task_title=task.get("title"),
1151
+ related_user_name=assignee_name
1152
+ )
1153
+ initiator_amount = deposit
1154
+ respondent_amount = 0
1155
+ else:
1156
+ # 接单者胜诉:获得全款
1157
  remaining = total_price - deposit
1158
+ # 发布者支付尾款(从冻结余额扣除)
1159
+ publisher_wallet.frozen_balance = max(0, publisher_wallet.frozen_balance - remaining)
1160
+ # 接单者获得全款
1161
+ assignee_wallet.balance += total_price
1162
+ assignee_wallet.earn_balance += total_price
1163
+
1164
+ # 记录交易
1165
+ create_task_transaction(
1166
+ db_session, publisher_account, "TASK_PAYMENT",
1167
+ -remaining, related_account=assignee_account, task_id=task.get("id"),
1168
+ task_title=task.get("title"),
1169
+ related_user_name=assignee_name
1170
+ )
1171
+ create_task_transaction(
1172
+ db_session, assignee_account, "TASK_INCOME",
1173
+ total_price, related_account=publisher_account, task_id=task.get("id"),
1174
+ task_title=task.get("title"),
1175
+ related_user_name=publisher_name
1176
+ )
1177
+ initiator_amount = total_price
1178
+ respondent_amount = 0
1179
+
1180
+ elif resolution == "favor_respondent":
1181
+ # 支持被申诉方
1182
+ if is_publisher_initiator:
1183
+ # 接单者胜诉:获得全款
1184
  remaining = total_price - deposit
1185
+ # 发布者支付尾款(从冻结余额扣除)
1186
+ publisher_wallet.frozen_balance = max(0, publisher_wallet.frozen_balance - remaining)
1187
+ # 接单者获得全款
1188
+ assignee_wallet.balance += total_price
1189
+ assignee_wallet.earn_balance += total_price
1190
+
1191
+ # 记录交易
1192
+ create_task_transaction(
1193
+ db_session, publisher_account, "TASK_PAYMENT",
1194
+ -remaining, related_account=assignee_account, task_id=task.get("id"),
1195
+ task_title=task.get("title"),
1196
+ related_user_name=assignee_name
1197
+ )
1198
+ create_task_transaction(
1199
+ db_session, assignee_account, "TASK_INCOME",
1200
+ total_price, related_account=publisher_account, task_id=task.get("id"),
1201
+ task_title=task.get("title"),
1202
+ related_user_name=publisher_name
1203
+ )
1204
+ initiator_amount = 0
1205
+ respondent_amount = total_price
1206
+ else:
1207
+ # 发布者胜诉:退还订金(从冻结余额释放到可用余额)
1208
+ publisher_wallet.frozen_balance = max(0, publisher_wallet.frozen_balance - deposit)
1209
+ publisher_wallet.balance += deposit
1210
+
1211
+ # 记录交易
1212
+ create_task_transaction(
1213
+ db_session, publisher_account, "TASK_REFUND",
1214
+ deposit, task_id=task.get("id"),
1215
+ task_title=task.get("title"),
1216
+ related_user_name=assignee_name
1217
+ )
1218
+ initiator_amount = 0
1219
+ respondent_amount = deposit
1220
+
1221
  else:
1222
+ # 按比例分成
1223
+ initiator_share = int(total_price * ratio / 100)
1224
+ respondent_share = total_price - initiator_share
 
 
 
 
 
 
 
 
 
1225
  remaining = total_price - deposit
1226
+
1227
+ # 发布者支付尾款(从冻结余额扣除)
1228
+ publisher_wallet.frozen_balance = max(0, publisher_wallet.frozen_balance - remaining)
1229
+
1230
+ # 发布者获得退款部分,接单者获得报酬部分
1231
+ if is_publisher_initiator:
1232
+ # 申诉方是发布者
1233
+ pub_refund = initiator_share
1234
+ assignee_earn = respondent_share
1235
+ else:
1236
+ # 申诉方是接单者
1237
+ assignee_earn = initiator_share
1238
+ pub_refund = respondent_share
1239
+
1240
+ # 分配资金
1241
+ publisher_wallet.balance += pub_refund
1242
+ assignee_wallet.balance += assignee_earn
1243
+ assignee_wallet.earn_balance += assignee_earn
1244
+
1245
+ # 记录交易
1246
+ create_task_transaction(
1247
+ db_session, publisher_account, "TASK_PAYMENT",
1248
+ -remaining, related_account=assignee_account, task_id=task.get("id"),
1249
+ task_title=task.get("title"),
1250
+ related_user_name=assignee_name
1251
+ )
1252
+ create_task_transaction(
1253
+ db_session, assignee_account, "TASK_INCOME",
1254
+ assignee_earn, related_account=publisher_account, task_id=task.get("id"),
1255
+ task_title=task.get("title"),
1256
+ related_user_name=publisher_name
1257
+ )
1258
+
1259
+ initiator_amount = initiator_share
1260
+ respondent_amount = respondent_share
1261
 
1262
+ # 提交 SQL 事务
1263
+ db_session.commit()
 
 
1264
 
1265
+ except Exception as e:
1266
+ db_session.rollback()
1267
+ logger.error(f"DISPUTE_RESOLVE_ERROR | dispute={dispute_id} | error={str(e)}")
1268
+ raise HTTPException(status_code=500, detail="仲裁资金处理失败,请稍后重试")
1269
 
1270
  # 更新申诉状态
1271
  dispute["resolution"] = resolution
 
1282
 
1283
  db.save_data("disputes.json", disputes_db)
1284
  db.save_data("tasks.json", tasks_db)
 
1285
  # 🗂️ 清除排序缓存(任务状态变为已完成)
1286
  sort_cache.invalidate("tasks:")
1287
 
 
1311
  "content": f"任务申诉已裁决:{resolution_text},您获得{respondent_amount}积分"
1312
  })
1313
 
1314
+ logger.info(f"DISPUTE_RESOLVED | dispute={dispute_id} | resolution={resolution} | initiator_amount={initiator_amount} | respondent_amount={respondent_amount}")
1315
+
1316
  return {"status": "success", "message": f"裁决完成:{resolution_text}"}
1317
 
1318
  raise HTTPException(status_code=404, detail="申诉不存在")
 
1440
  # ==========================================
1441
 
1442
  @router.post("/api/tasks/{task_id}/tip")
1443
+ async def tip_task(task_id: str, amount: int, is_anon: bool = False, current_user: str = Depends(require_auth), db_session: Session = Depends(get_db)):
1444
  """
1445
+ 打赏任务(原子操作,并发安全)- 使用 SQL Wallet 系统
1446
  """
1447
  if amount <= 0:
1448
  raise HTTPException(status_code=400, detail="打赏金额必须大于0")
1449
 
1450
  result_container = [None]
1451
+ publisher_account = [None] # 用于在原子操作外获取发布者账号
1452
 
1453
  def updater(data):
1454
  # 在锁内查找任务
 
1467
  result_container[0] = {"error": "self_tip"}
1468
  return
1469
 
1470
+ publisher_account[0] = target_task.get("publisher")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1471
 
1472
  # 更新打赏榜单
1473
  tip_board = target_task.get("tip_board", [])
 
1479
  tip_board.sort(key=lambda x: x["amount"], reverse=True)
1480
  target_task["tip_board"] = tip_board
1481
 
 
 
 
1482
  result_container[0] = {"status": "success", "message": f"成功打赏 {amount} 积分"}
1483
 
1484
  db.atomic_update("tasks.json", updater, default_data=[])
1485
 
 
 
 
1486
  result = result_container[0]
1487
  if result is None or result.get("error") == "not_found":
1488
  raise HTTPException(status_code=404, detail="任务不存在")
1489
  if result.get("error") == "self_tip":
1490
  raise HTTPException(status_code=400, detail="不能打赏自己的任务")
1491
+
1492
+ # 💳 使用 SQL Wallet 系统处理余额转账
1493
+ try:
1494
+ publisher = publisher_account[0]
1495
+ if not publisher:
1496
+ raise HTTPException(status_code=404, detail="作者账户不存在")
1497
+
1498
+ # 🔒 P1幂等性防护:检查最近5秒内是否存在相同交易
1499
+ recent_cutoff = datetime.datetime.utcnow() - datetime.timedelta(seconds=5)
1500
+ duplicate_tx = db_session.query(Transaction).filter(
1501
+ Transaction.account == current_user,
1502
+ Transaction.tx_type == "TIP_OUT",
1503
+ Transaction.amount == -amount,
1504
+ Transaction.related_account == publisher,
1505
+ Transaction.created_at >= recent_cutoff
1506
+ ).first()
1507
+ if duplicate_tx:
1508
+ return {"status": "success", "message": "打赏已处理(重复请求)"}
1509
+
1510
+ # 🔒 并发安全:使用悲观锁获取双方钱包
1511
+ tipper_wallet = db_session.query(Wallet).filter(Wallet.account == current_user).with_for_update().first()
1512
+ author_wallet = db_session.query(Wallet).filter(Wallet.account == publisher).with_for_update().first()
1513
+
1514
+ if not tipper_wallet or tipper_wallet.balance < amount:
1515
+ raise HTTPException(status_code=400, detail="余额不足")
1516
+ if not author_wallet:
1517
+ # 如果作者钱包不存在,创建一个
1518
+ author_wallet = Wallet(account=publisher, balance=0, earn_balance=0, tip_balance=0, frozen_balance=0)
1519
+ db_session.add(author_wallet)
1520
+
1521
+ # 执行转账
1522
+ tipper_wallet.balance -= amount
1523
+ author_wallet.balance += amount # 实际收入进统一余额
1524
+ author_wallet.tip_balance += amount # 累计打赏收益统计(只增不减)
1525
+
1526
+ # 创建交易记录
1527
+ tx_id_tipper = f"TIP_OUT_{int(time.time())}_{uuid.uuid4().hex[:6]}"
1528
+ tx_id_author = f"TIP_IN_{int(time.time())}_{uuid.uuid4().hex[:6]}"
1529
+
1530
+ # 获取最后交易记录的哈希
1531
+ last_tx_tipper = db_session.query(Transaction).filter(Transaction.account == current_user).order_by(Transaction.created_at.desc()).first()
1532
+ last_tx_author = db_session.query(Transaction).filter(Transaction.account == publisher).order_by(Transaction.created_at.desc()).first()
1533
+ prev_hash_tipper = last_tx_tipper.tx_hash if last_tx_tipper else "GENESIS_HASH"
1534
+ prev_hash_author = last_tx_author.tx_hash if last_tx_author else "GENESIS_HASH"
1535
+
1536
+ # 获取用户信息和任务标题
1537
+ users_db = db.load_data("users.json", default_data={})
1538
+ tasks_db = db.load_data("tasks.json", default_data=[])
1539
+ author_info = users_db.get(publisher, {})
1540
+ tipper_info = users_db.get(current_user, {})
1541
+ author_name = author_info.get("name", publisher)
1542
+ tipper_name = tipper_info.get("name", current_user)
1543
+
1544
+ task_title = None
1545
+ for task in tasks_db:
1546
+ if task["id"] == task_id:
1547
+ task_title = task.get("title")
1548
+ break
1549
+
1550
+ # 打赏方交易记录 (TIP_OUT)
1551
+ tx_tipper = Transaction(
1552
+ tx_id=tx_id_tipper,
1553
+ account=current_user,
1554
+ tx_type="TIP_OUT",
1555
+ amount=-amount,
1556
+ related_account=publisher,
1557
+ item_id=task_id,
1558
+ prev_hash=prev_hash_tipper,
1559
+ tx_hash=calculate_tx_hash(tx_id_tipper, current_user, "TIP_OUT", -amount, prev_hash_tipper),
1560
+ description=f"打赏给 {author_name}" + (f" 的任务《{task_title}》" if task_title else ""),
1561
+ item_title=task_title,
1562
+ item_type="task",
1563
+ related_user_name=author_name
1564
+ )
1565
+
1566
+ # 接收方交易记录 (TIP_IN)
1567
+ tx_author = Transaction(
1568
+ tx_id=tx_id_author,
1569
+ account=publisher,
1570
+ tx_type="TIP_IN",
1571
+ amount=amount,
1572
+ related_account=current_user,
1573
+ item_id=task_id,
1574
+ prev_hash=prev_hash_author,
1575
+ tx_hash=calculate_tx_hash(tx_id_author, publisher, "TIP_IN", amount, prev_hash_author),
1576
+ description=f"收到 {tipper_name} 的任务打赏" + (f" ({task_title})" if task_title else ""),
1577
+ item_title=task_title,
1578
+ item_type="task",
1579
+ related_user_name=tipper_name if not is_anon else "匿名用户"
1580
+ )
1581
+
1582
+ db_session.add(tx_tipper)
1583
+ db_session.add(tx_author)
1584
+ db_session.commit()
1585
+
1586
+ # 📝 审计日志
1587
+ logger.info(f"TASK_TIP | from={current_user} | to={publisher} | amount={amount} | task={task_id} | anon={is_anon}")
1588
+
1589
+ # 🔔 打赏通知(考虑匿名)
1590
+ if not is_anon:
1591
+ add_notification(publisher, {
1592
+ "type": "tip",
1593
+ "from_user": current_user,
1594
+ "target_item_id": task_id,
1595
+ "target_item_title": task_title or "",
1596
+ "content": f"您收到来自 {tipper_name} 的 {amount} 积分任务打赏"
1597
+ })
1598
+ else:
1599
+ add_notification(publisher, {
1600
+ "type": "tip",
1601
+ "from_user": "anonymous",
1602
+ "target_item_id": task_id,
1603
+ "target_item_title": "",
1604
+ "content": f"您收到了一份 {amount} 积分的匿名任务打赏"
1605
+ })
1606
+
1607
+ # 🗂️ 清除排序缓存(打赏可能影响排序)
1608
+ sort_cache.invalidate("tasks:")
1609
+
1610
+ except HTTPException:
1611
+ db_session.rollback()
1612
+ raise
1613
+ except Exception as e:
1614
+ db_session.rollback()
1615
+ logger.error(f"TASK_TIP_ERROR | from={current_user} | task={task_id} | amount={amount} | error={str(e)}")
1616
+ raise HTTPException(status_code=500, detail="打赏处理失败,请稍后重试")
1617
 
1618
  return result
1619
 
router_wallet.py CHANGED
@@ -292,7 +292,20 @@ async def purchase_item(request: Request, req: PurchaseRequest, db: Session = De
292
  seller_account = item.get("author")
293
 
294
  if price <= 0 or req.account == seller_account:
295
- # ☁️ 免费资源或作者本人,也返回网盘密码
 
 
 
 
 
 
 
 
 
 
 
 
 
296
  return {
297
  "status": "success",
298
  "already_owned": True,
@@ -337,7 +350,8 @@ async def purchase_item(request: Request, req: PurchaseRequest, db: Session = De
337
  db.add(seller_wallet)
338
 
339
  buyer_wallet.balance = (buyer_wallet.balance or 0) - price
340
- seller_wallet.earn_balance = (seller_wallet.earn_balance or 0) + price
 
341
 
342
  # 🔄 P7后悔模式:记录购买价格
343
  new_ownership = Ownership(account=req.account, item_id=req.item_id, price_paid=price)
@@ -401,6 +415,18 @@ async def tip_user(request: Request, req: TipRequest, db: Session = Depends(get_
401
  if req.sender_account == req.target_account:
402
  raise HTTPException(status_code=400, detail="不能打赏给自己")
403
 
 
 
 
 
 
 
 
 
 
 
 
 
404
  # 🔒 P1并发安全:使用悲观锁+异常处理+事务回滚防止双花问题
405
  try:
406
  sender_wallet = db.query(Wallet).filter(Wallet.account == req.sender_account).with_for_update().first()
@@ -413,7 +439,8 @@ async def tip_user(request: Request, req: TipRequest, db: Session = Depends(get_
413
  db.add(target_wallet)
414
 
415
  sender_wallet.balance -= req.amount
416
- target_wallet.tip_balance += req.amount
 
417
 
418
  tx_id_sender = f"TIP_OUT_{int(time.time())}_{uuid.uuid4().hex[:6]}"
419
  tx_id_target = f"TIP_IN_{int(time.time())}_{uuid.uuid4().hex[:6]}"
@@ -538,6 +565,21 @@ async def tip_user(request: Request, req: TipRequest, db: Session = Depends(get_
538
  @router.post("/api/wallet/withdraw")
539
  @limiter.limit("3/minute") # 🔒 P0安全优化:提现每分钟最多3次
540
  async def withdraw(request: Request, req: WithdrawRequest, db: Session = Depends(get_db)):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
541
  # 🔒 验证码缓存键格式:{contact}_{action_type},与 send_code 接口一致
542
  # 先通过账号查询用户邮箱,再用邮箱构建缓存键
543
  users_db = json_db.load_data("users.json", default_data={})
@@ -566,8 +608,8 @@ async def withdraw(request: Request, req: WithdrawRequest, db: Session = Depends
566
  ).scalar() or 0
567
  total_withdrawn = abs(withdrawals_sum)
568
 
569
- # 手续费规则:100元 = 10000积分 免手续费额度,超出部分收取 10%
570
- free_quota = max(0, 10000 - total_withdrawn) # 剩余免责额度
571
  fee_amount = 0
572
  if req.amount > free_quota:
573
  fee_amount = int((req.amount - free_quota) * 0.10) # 只对超出部分收 10%
@@ -575,17 +617,11 @@ async def withdraw(request: Request, req: WithdrawRequest, db: Session = Depends
575
  actual_withdraw = req.amount # 从账户扣除的金额
576
  net_amount = req.amount - fee_amount # 用户实际到账金额
577
 
578
- total_withdrawable = wallet.earn_balance + wallet.tip_balance
579
- if actual_withdraw > total_withdrawable:
580
  raise HTTPException(status_code=400, detail="可提现余额不足")
581
 
582
- if actual_withdraw <= wallet.earn_balance:
583
- wallet.earn_balance -= actual_withdraw
584
- else:
585
- remaining = actual_withdraw - wallet.earn_balance
586
- wallet.earn_balance = 0
587
- wallet.tip_balance -= remaining
588
-
589
  wallet.frozen_balance += net_amount # 冻结的是到账金额,非手续费部分
590
 
591
  tx_id = f"WD_{int(time.time())}_{uuid.uuid4().hex[:6]}"
@@ -880,16 +916,11 @@ async def refund_purchase(request: Request, account: str, item_id: str, db: Sess
880
  seller_wallet = db.query(Wallet).filter(Wallet.account == seller_account).with_for_update().first()
881
 
882
  if seller_wallet:
883
- # 卖家收益中除(如果不足则从余额扣除)
884
- if seller_wallet.earn_balance >= refund_amount:
885
- seller_wallet.earn_balance -= refund_amount
886
- else:
887
- remaining = refund_amount - seller_wallet.earn_balance
888
- seller_wallet.earn_balance = 0
889
- seller_wallet.balance = max(0, seller_wallet.balance - remaining)
890
 
891
  if buyer_wallet:
892
- buyer_wallet.balance += refund_amount
893
  else:
894
  buyer_wallet = Wallet(account=account, balance=refund_amount)
895
  db.add(buyer_wallet)
 
292
  seller_account = item.get("author")
293
 
294
  if price <= 0 or req.account == seller_account:
295
+ # ☁️ 免费资源或作者本人,确保 Ownership 记录存在
296
+ existing_ownership = db.query(Ownership).filter(
297
+ Ownership.account == req.account,
298
+ Ownership.item_id == req.item_id,
299
+ Ownership.is_refunded == False
300
+ ).first()
301
+ if not existing_ownership:
302
+ new_ownership = Ownership(
303
+ account=req.account,
304
+ item_id=req.item_id,
305
+ price_paid=0
306
+ )
307
+ db.add(new_ownership)
308
+ db.commit()
309
  return {
310
  "status": "success",
311
  "already_owned": True,
 
350
  db.add(seller_wallet)
351
 
352
  buyer_wallet.balance = (buyer_wallet.balance or 0) - price
353
+ seller_wallet.balance = (seller_wallet.balance or 0) + price # 实际收入进统一余额
354
+ seller_wallet.earn_balance = (seller_wallet.earn_balance or 0) + price # 累计销售收益统计(只增不减)
355
 
356
  # 🔄 P7后悔模式:记录购买价格
357
  new_ownership = Ownership(account=req.account, item_id=req.item_id, price_paid=price)
 
415
  if req.sender_account == req.target_account:
416
  raise HTTPException(status_code=400, detail="不能打赏给自己")
417
 
418
+ # 🔒 P1幂等性防护:检查最近5秒内是否存在相同交易
419
+ recent_cutoff = datetime.datetime.utcnow() - datetime.timedelta(seconds=5)
420
+ duplicate_tx = db.query(Transaction).filter(
421
+ Transaction.account == req.sender_account,
422
+ Transaction.tx_type == "TIP_OUT",
423
+ Transaction.amount == -req.amount,
424
+ Transaction.related_account == req.target_account,
425
+ Transaction.created_at >= recent_cutoff
426
+ ).first()
427
+ if duplicate_tx:
428
+ return {"status": "success", "message": "打赏已处理(重复请求)"}
429
+
430
  # 🔒 P1并发安全:使用悲观锁+异常处理+事务回滚防止双花问题
431
  try:
432
  sender_wallet = db.query(Wallet).filter(Wallet.account == req.sender_account).with_for_update().first()
 
439
  db.add(target_wallet)
440
 
441
  sender_wallet.balance -= req.amount
442
+ target_wallet.balance += req.amount # 实际收入进统一余额
443
+ target_wallet.tip_balance += req.amount # 累计打赏收益统计(只增不减)
444
 
445
  tx_id_sender = f"TIP_OUT_{int(time.time())}_{uuid.uuid4().hex[:6]}"
446
  tx_id_target = f"TIP_IN_{int(time.time())}_{uuid.uuid4().hex[:6]}"
 
565
  @router.post("/api/wallet/withdraw")
566
  @limiter.limit("3/minute") # 🔒 P0安全优化:提现每分钟最多3次
567
  async def withdraw(request: Request, req: WithdrawRequest, db: Session = Depends(get_db)):
568
+ # 🔒 P1幂等性防护:检查最近10秒内是否存在相同提现请求
569
+ recent_cutoff = datetime.datetime.utcnow() - datetime.timedelta(seconds=10)
570
+ duplicate_tx = db.query(Transaction).filter(
571
+ Transaction.account == req.account,
572
+ Transaction.tx_type == "WITHDRAW",
573
+ Transaction.amount == -req.amount,
574
+ Transaction.created_at >= recent_cutoff
575
+ ).first()
576
+ if duplicate_tx:
577
+ return {"status": "success", "message": "提现申请已提交(重复请求)"}
578
+
579
+ # 🔒 最小金额检查
580
+ if req.amount < 1:
581
+ raise HTTPException(status_code=400, detail="最小提现金额为1积分")
582
+
583
  # 🔒 验证码缓存键格式:{contact}_{action_type},与 send_code 接口一致
584
  # 先通过账号查询用户邮箱,再用邮箱构建缓存键
585
  users_db = json_db.load_data("users.json", default_data={})
 
608
  ).scalar() or 0
609
  total_withdrawn = abs(withdrawals_sum)
610
 
611
+ # 手续费规则:100积分免手续费额度,超出部分收取 10%
612
+ free_quota = max(0, 100 - total_withdrawn) # 剩余免责额度
613
  fee_amount = 0
614
  if req.amount > free_quota:
615
  fee_amount = int((req.amount - free_quota) * 0.10) # 只对超出部分收 10%
 
617
  actual_withdraw = req.amount # 从账户扣除的金额
618
  net_amount = req.amount - fee_amount # 用户实际到账金额
619
 
620
+ # 可提现额度改为 wallet.balance(统一余额)
621
+ if actual_withdraw > wallet.balance:
622
  raise HTTPException(status_code=400, detail="可提现余额不足")
623
 
624
+ wallet.balance -= actual_withdraw # 直接从统一余额扣除
 
 
 
 
 
 
625
  wallet.frozen_balance += net_amount # 冻结的是到账金额,非手续费部分
626
 
627
  tx_id = f"WD_{int(time.time())}_{uuid.uuid4().hex[:6]}"
 
916
  seller_wallet = db.query(Wallet).filter(Wallet.account == seller_account).with_for_update().first()
917
 
918
  if seller_wallet:
919
+ seller_wallet.balance -= refund_amount # 卖家扣
920
+ seller_wallet.earn_balance = max(0, seller_wallet.earn_balance - refund_amount) # 累计统计回退(防止负数)
 
 
 
 
 
921
 
922
  if buyer_wallet:
923
+ buyer_wallet.balance += refund_amount # 买家退款
924
  else:
925
  buyer_wallet = Wallet(account=account, balance=refund_amount)
926
  db.add(buyer_wallet)