ZHIWEI666 commited on
Commit
c6147cc
·
verified ·
1 Parent(s): 43299fb

Upload router_wallet.py

Browse files
Files changed (1) hide show
  1. router_wallet.py +779 -744
router_wallet.py CHANGED
@@ -1,745 +1,780 @@
1
- # router_wallet.py
2
- # ==========================================
3
- # 💰 钱包与交易路由模块
4
- # ==========================================
5
- # 作用:处理充值、提现、购买、打赏等资金操作
6
- # 关联文件:
7
- # - verify_code_engine.py (提现验证码缓存)
8
- # - database_sql.py (SQL数据库连接)
9
- # - models_sql.py (Wallet, Transaction, Ownership 模型)
10
- # 🔒 P0安全优化:API限流
11
- # ==========================================
12
-
13
- from fastapi import APIRouter, Depends, HTTPException, Request
14
- from fastapi.responses import Response
15
- from sqlalchemy.orm import Session
16
- import time
17
- import uuid
18
- import hashlib
19
- import os
20
- import datetime
21
- import logging
22
- from database_sql import get_db
23
- from models_sql import Wallet, Transaction, Ownership, Refund
24
- from models import RechargeRequest, WithdrawRequest, PurchaseRequest, TipRequest
25
- import 数据库连接 as json_db
26
-
27
- # 🔒 P0安全优化:API限流
28
- from slowapi import Limiter
29
- from slowapi.util import get_remote_address
30
- limiter = Limiter(key_func=get_remote_address)
31
-
32
- # 🔄 P7后悔模式:24小时退款窗口
33
- REFUND_WINDOW_HOURS = 24
34
- # 🔄 P7后悔模式:退款后30天禁购
35
- REFUND_BAN_DAYS = 30
36
-
37
- # 📝 P2优化:审计日志
38
- logger = logging.getLogger("ComfyUI-Ranking.Wallet")
39
-
40
- # 🔐 导入验证码缓存 (提现时需要验证)
41
- from verify_code_engine import VERIFY_CODES
42
-
43
- router = APIRouter()
44
-
45
- # ==========================================
46
- # 🚨 替换这里的支付宝初始化逻辑 🚨
47
- # ==========================================
48
- def format_pem_key(key_str, key_type="PRIVATE"):
49
- """自动给'裸奔'的秘钥穿上符合 Python 规范的外套"""
50
- # 如果用户已经带有 BEGIN 标签,说明格式没问题,直接返回
51
- if "BEGIN" in key_str:
52
- return key_str.replace("\\n", "\n").strip()
53
-
54
- # 去除所有的空格和换行符,拿到最纯净的字符串
55
- clean_key = key_str.replace(" ", "").replace("\n", "").replace("\\n", "").replace("\r", "")
56
-
57
- # 按照国际标准,每 64 个字符强制切断换行
58
- chunks = [clean_key[i:i+64] for i in range(0, len(clean_key), 64)]
59
- formatted_body = "\n".join(chunks)
60
-
61
- # 穿上头尾外套
62
- if key_type == "PRIVATE":
63
- return f"-----BEGIN RSA PRIVATE KEY-----\n{formatted_body}\n-----END RSA PRIVATE KEY-----"
64
- else:
65
- return f"-----BEGIN PUBLIC KEY-----\n{formatted_body}\n-----END PUBLIC KEY-----"
66
-
67
- alipay_error_msg = "未知错误"
68
- try:
69
- from alipay import AliPay
70
- from alipay.utils import AliPayConfig
71
-
72
- # 1. 抓取原生态的环境变量
73
- raw_appid = os.environ.get("ALIPAY_APPID", "").strip()
74
- raw_priv_key = os.environ.get("ALIPAY_PRIVATE_KEY", "").strip()
75
- raw_pub_key = os.environ.get("ALIPAY_PUBLIC_KEY", "").strip()
76
-
77
- if not raw_appid or not raw_priv_key or not raw_pub_key:
78
- alipay_error_msg = f"缺少环境变量。当前读取到: APPID={bool(raw_appid)}, PRIV_KEY={bool(raw_priv_key)}, PUB_KEY={bool(raw_pub_key)}"
79
- alipay = None
80
- else:
81
- # 2. 扔进我们的格式化引擎进行自动包装!
82
- priv_key_formatted = format_pem_key(raw_priv_key, "PRIVATE")
83
- pub_key_formatted = format_pem_key(raw_pub_key, "PUBLIC")
84
-
85
- # 3. 完美加载
86
- alipay = AliPay(
87
- appid=raw_appid,
88
- app_notify_url="https://zhiwei666-comfyui-ranking-api.hf.space/api/wallet/alipay_notify",
89
- app_private_key_string=priv_key_formatted,
90
- alipay_public_key_string=pub_key_formatted,
91
- sign_type="RSA2",
92
- debug=False,
93
- config=AliPayConfig(timeout=15)
94
- )
95
- except Exception as e:
96
- alipay = None
97
- alipay_error_msg = f"支付宝 SDK 崩溃: {str(e)}"
98
- print(f"🚨 支付宝初始化异常: {alipay_error_msg}")
99
-
100
- def calculate_tx_hash(tx_id, account, tx_type, amount, prev_hash):
101
- data = f"{tx_id}{account}{tx_type}{amount}{prev_hash}"
102
- return hashlib.sha256(data.encode()).hexdigest()
103
-
104
- @router.post("/api/wallet/create_recharge_order")
105
- async def create_recharge_order(req: RechargeRequest):
106
- if not alipay:
107
- # 这里会将真实的错误原因直接弹窗发给前端!
108
- raise HTTPException(status_code=500, detail=f"支付网关配置错误: {alipay_error_msg}")
109
-
110
- order_id = f"PAY_{int(time.time())}_{uuid.uuid4().hex[:6]}"
111
- subject = f"ComfyUI Community Points - {req.account}"
112
-
113
- order_string = alipay.api_alipay_trade_precreate(
114
- out_trade_no=order_id,
115
- total_amount=str(req.amount),
116
- subject=subject
117
- )
118
-
119
- qr_code_url = order_string.get("qr_code")
120
- if not qr_code_url:
121
- raise HTTPException(status_code=500, detail="生成支付二维码失败")
122
-
123
- return {"status": "success", "order_id": order_id, "qr_code": qr_code_url}
124
-
125
- # 🟢 业务流转细节修复:正确解析 application/x-www-form-urlencoded
126
- @router.post("/api/wallet/alipay_notify")
127
- async def alipay_notify(request: Request, db: Session = Depends(get_db)):
128
- # 强制将表单数据解析为纯字典,防止由于数据类型错误导致验签失败
129
- form_data = await request.form()
130
- data = dict(form_data.items())
131
-
132
- signature = data.pop("sign", None)
133
- data.pop("sign_type", None)
134
-
135
- if not alipay or not signature or not alipay.verify(data, signature):
136
- return Response(content="fail", media_type="text/plain")
137
-
138
- if data.get("trade_status") in ("TRADE_SUCCESS", "TRADE_FINISHED"):
139
- order_id = data.get("out_trade_no")
140
-
141
- existing_tx = db.query(Transaction).filter(Transaction.tx_id == order_id).first()
142
- if not existing_tx:
143
- amount = int(float(data.get("total_amount", 0)))
144
- account = data.get("subject", "").split(" - ")[-1]
145
-
146
- # 🔒 P1并发安全:使用悲观锁+异常处理+事务回滚防止并发充值问题
147
- try:
148
- wallet = db.query(Wallet).filter(Wallet.account == account).with_for_update().first()
149
- if not wallet:
150
- wallet = Wallet(account=account)
151
- db.add(wallet)
152
-
153
- wallet.balance += amount
154
-
155
- last_tx = db.query(Transaction).filter(Transaction.account == account).order_by(Transaction.created_at.desc()).first()
156
- prev_hash = last_tx.tx_hash if last_tx else "GENESIS_HASH"
157
- tx_hash = calculate_tx_hash(order_id, account, "RECHARGE", amount, prev_hash)
158
-
159
- new_tx = Transaction(
160
- tx_id=order_id, account=account, tx_type="RECHARGE", amount=amount,
161
- prev_hash=prev_hash, tx_hash=tx_hash
162
- )
163
- db.add(new_tx)
164
- db.commit()
165
-
166
- # 📝 P2优化:充值审计日志
167
- logger.info(f"RECHARGE | account={account} | amount={amount} | order={order_id}")
168
- except Exception as e:
169
- db.rollback()
170
- logger.error(f"RECHARGE_ERROR | account={account} | amount={amount} | order={order_id} | error={str(e)}")
171
- return Response(content="fail", media_type="text/plain")
172
-
173
- return Response(content="success", media_type="text/plain")
174
-
175
- @router.get("/api/wallet/check_order/{order_id}")
176
- async def check_order(order_id: str, db: Session = Depends(get_db)):
177
- tx = db.query(Transaction).filter(Transaction.tx_id == order_id).first()
178
- if tx:
179
- return {"status": "SUCCESS"}
180
- return {"status": "PENDING"}
181
-
182
- @router.get("/api/wallet/{account}")
183
- async def get_wallet(account: str, db: Session = Depends(get_db)):
184
- wallet = db.query(Wallet).filter(Wallet.account == account).first()
185
-
186
- # 🚀 P1性能优化:使用聚合函数代替 .all() + sum
187
- from sqlalchemy import func
188
- total_withdrawn = db.query(func.coalesce(func.sum(Transaction.amount), 0)).filter(
189
- Transaction.account == account,
190
- Transaction.tx_type == 'WITHDRAW'
191
- ).scalar() or 0
192
- total_withdrawn = abs(total_withdrawn) # 提现金额是负数
193
-
194
- if not wallet:
195
- return {"status": "success", "balance": 0, "earn_balance": 0, "tip_balance": 0, "frozen_balance": 0, "total_withdrawn": total_withdrawn}
196
-
197
- return {
198
- "status": "success",
199
- "balance": wallet.balance,
200
- "earn_balance": wallet.earn_balance,
201
- "tip_balance": wallet.tip_balance,
202
- "frozen_balance": wallet.frozen_balance,
203
- "total_withdrawn": total_withdrawn # 暴露给前端
204
- }
205
-
206
- @router.post("/api/wallet/purchase")
207
- @limiter.limit("10/minute") # 🔒 P0安全优化:购买每分钟最多10次
208
- async def purchase_item(request: Request, req: PurchaseRequest, db: Session = Depends(get_db)):
209
- items_db = json_db.load_data("items.json", default_data=[])
210
- item = next((i for i in items_db if i["id"] == req.item_id), None)
211
-
212
- if not item:
213
- raise HTTPException(status_code=404, detail="商品不存在")
214
-
215
- # 🔄 P7后悔模式:检查价格是否延迟生效
216
- actual_price = item.get("price", 0)
217
- pending_price = item.get("pending_price")
218
- pending_price_effective = item.get("pending_price_effective_at")
219
- if pending_price is not None and pending_price_effective:
220
- # 检查是否已过生效时间
221
- effective_time = datetime.datetime.fromisoformat(pending_price_effective)
222
- if datetime.datetime.now() >= effective_time:
223
- actual_price = pending_price
224
- # 更新实际价格,清除待生效价格
225
- item["price"] = pending_price
226
- item["pending_price"] = None
227
- item["pending_price_effective_at"] = None
228
- json_db.save_data("items.json", items_db)
229
-
230
- price = int(actual_price)
231
- seller_account = item.get("author")
232
-
233
- if price <= 0 or req.account == seller_account:
234
- # ☁️ 免费资源或作者本人,也返回网盘密码
235
- return {
236
- "status": "success",
237
- "already_owned": True,
238
- "netdisk_password": item.get("netdisk_password"), # ☁️
239
- "is_netdisk": item.get("is_netdisk", False) # ☁️
240
- }
241
-
242
- # 🔄 P7后悔模式检查30天禁
243
- refund_ban = db.query(Refund).filter(
244
- Refund.account == req.account,
245
- Refund.item_id == req.item_id,
246
- Refund.ban_until > datetime.datetime.utcnow()
247
- ).first()
248
- if refund_ban:
249
- days_left = (refund_ban.ban_until - datetime.datetime.utcnow()).days + 1
250
- raise HTTPException(status_code=403, detail=f"您已退款过此商品,{days_left}天内禁止再次购买")
251
-
252
- # 检查是否已拥有(排除已退款的记录)
253
- owned = db.query(Ownership).filter(
254
- Ownership.account == req.account,
255
- Ownership.item_id == req.item_id,
256
- Ownership.is_refunded == False
257
- ).first()
258
- if owned:
259
- # ☁️ 已购买用户返回网盘密码
260
- return {
261
- "status": "success",
262
- "already_owned": True,
263
- "netdisk_password": item.get("netdisk_password"), # ☁️
264
- "is_netdisk": item.get("is_netdisk", False) # ☁️
265
- }
266
-
267
- # 🔒 P1并发安全:使用悲观锁+异常处理+事务回滚防止双花问题
268
- try:
269
- buyer_wallet = db.query(Wallet).filter(Wallet.account == req.account).with_for_update().first()
270
- if not buyer_wallet or buyer_wallet.balance < price:
271
- raise HTTPException(status_code=402, detail="余额不足,请先充值")
272
-
273
- seller_wallet = db.query(Wallet).filter(Wallet.account == seller_account).with_for_update().first()
274
- if not seller_wallet:
275
- seller_wallet = Wallet(account=seller_account)
276
- db.add(seller_wallet)
277
-
278
- buyer_wallet.balance -= price
279
- seller_wallet.earn_balance += price
280
-
281
- # 🔄 P7后悔模式:记录购买价格
282
- new_ownership = Ownership(account=req.account, item_id=req.item_id, price_paid=price)
283
- db.add(new_ownership)
284
-
285
- tx_id = f"BUY_{int(time.time())}_{uuid.uuid4().hex[:6]}"
286
- last_tx = db.query(Transaction).filter(Transaction.account == req.account).order_by(Transaction.created_at.desc()).first()
287
- prev_hash = last_tx.tx_hash if last_tx else "GENESIS_HASH"
288
- tx_hash = calculate_tx_hash(tx_id, req.account, "PURCHASE", -price, prev_hash)
289
-
290
- # 创建交易记录 (字段名为 related_account,与 models_sql.py Transaction 模型保持一致)
291
- new_tx = Transaction(
292
- tx_id=tx_id, account=req.account, tx_type="PURCHASE", amount=-price,
293
- related_account=seller_account, item_id=req.item_id, prev_hash=prev_hash, tx_hash=tx_hash
294
- )
295
- db.add(new_tx)
296
- db.commit()
297
-
298
- # 📝 P2优化:购买审计日志
299
- logger.info(f"PURCHASE | buyer={req.account} | seller={seller_account} | item={req.item_id} | amount={price} | tx={tx_id}")
300
-
301
- # ☁️ 购买成功后返回网盘密码
302
- return {
303
- "status": "success",
304
- "already_owned": False,
305
- "netdisk_password": item.get("netdisk_password"), # ☁️ 只有购买成功才返回
306
- "is_netdisk": item.get("is_netdisk", False) # ☁️
307
- }
308
- except HTTPException:
309
- db.rollback()
310
- raise
311
- except Exception as e:
312
- db.rollback()
313
- logger.error(f"PURCHASE_ERROR | buyer={req.account} | item={req.item_id} | error={str(e)}")
314
- raise HTTPException(status_code=500, detail="购买处理失败,请稍后重试")
315
-
316
- @router.post("/api/wallet/tip")
317
- @limiter.limit("20/minute") # 🔒 P0安全优化:打赏每分钟最多20次
318
- async def tip_user(request: Request, req: TipRequest, db: Session = Depends(get_db)):
319
- if req.amount <= 0:
320
- raise HTTPException(status_code=400, detail="打赏金额必须大于0")
321
- if req.sender_account == req.target_account:
322
- raise HTTPException(status_code=400, detail="不能打赏给自己")
323
-
324
- # 🔒 P1并发安全:使用悲观锁+异常处理+事务回滚防止双花问题
325
- try:
326
- sender_wallet = db.query(Wallet).filter(Wallet.account == req.sender_account).with_for_update().first()
327
- target_wallet = db.query(Wallet).filter(Wallet.account == req.target_account).with_for_update().first()
328
-
329
- if not sender_wallet or sender_wallet.balance < req.amount:
330
- raise HTTPException(status_code=400, detail="余额不足")
331
- if not target_wallet:
332
- target_wallet = Wallet(account=req.target_account, balance=0, earn_balance=0, tip_balance=0, frozen_balance=0)
333
- db.add(target_wallet)
334
-
335
- sender_wallet.balance -= req.amount
336
- target_wallet.tip_balance += req.amount
337
-
338
- tx_id_sender = f"TIP_OUT_{int(time.time())}_{uuid.uuid4().hex[:6]}"
339
- tx_id_target = f"TIP_IN_{int(time.time())}_{uuid.uuid4().hex[:6]}"
340
-
341
- # 记录交易
342
- last_tx_sender = db.query(Transaction).filter(Transaction.account == req.sender_account).order_by(Transaction.created_at.desc()).first()
343
- last_tx_target = db.query(Transaction).filter(Transaction.account == req.target_account).order_by(Transaction.created_at.desc()).first()
344
- prev_hash_sender = last_tx_sender.tx_hash if last_tx_sender else "GENESIS_HASH"
345
- prev_hash_target = last_tx_target.tx_hash if last_tx_target else "GENESIS_HASH"
346
-
347
- # 发送方交易记录 (字段名为 related_account,与 models_sql.py 中 Transaction 模型保持一致)
348
- tx_sender = Transaction(tx_id=tx_id_sender, account=req.sender_account, tx_type="TIP_OUT", amount=-req.amount,
349
- related_account=req.target_account, prev_hash=prev_hash_sender,
350
- tx_hash=calculate_tx_hash(tx_id_sender, req.sender_account, "TIP_OUT", -req.amount, prev_hash_sender))
351
-
352
- # 接收方交易记录
353
- tx_target = Transaction(tx_id=tx_id_target, account=req.target_account, tx_type="TIP_IN", amount=req.amount,
354
- related_account=req.sender_account, prev_hash=prev_hash_target,
355
- tx_hash=calculate_tx_hash(tx_id_target, req.target_account, "TIP_IN", req.amount, prev_hash_target))
356
-
357
- db.add(tx_sender)
358
- db.add(tx_target)
359
- db.commit()
360
-
361
- # 📝 P2优化:打赏审计日志
362
- logger.info(f"TIP | from={req.sender_account} | to={req.target_account} | amount={req.amount} | item={req.item_id or 'N/A'} | anon={req.is_anonymous}")
363
-
364
- # 🚀 核心新增:记录打赏榜单和月度收益趋势 (写入 JSON 以供高频读取)
365
- users_db = json_db.load_data("users.json", default_data={})
366
- items_db = json_db.load_data("items.json", default_data=[])
367
- current_month = datetime.date.today().strftime("%Y-%m")
368
-
369
- # 1. 更新创作者的总打赏榜与收益趋势
370
- if req.target_account in users_db:
371
- u = users_db[req.target_account]
372
- if "tip_history" not in u: u["tip_history"] = {}
373
- u["tip_history"][current_month] = u["tip_history"].get(current_month, 0) + req.amount
374
-
375
- if "tip_board" not in u: u["tip_board"] = []
376
- sender_entry = next((x for x in u["tip_board"] if x["account"] == req.sender_account), None)
377
- if sender_entry:
378
- sender_entry["amount"] += req.amount
379
- else:
380
- u["tip_board"].append({"account": req.sender_account, "amount": req.amount, "is_anon": req.is_anonymous})
381
- u["tip_board"] = sorted(u["tip_board"], key=lambda x: x["amount"], reverse=True)
382
- json_db.save_data("users.json", users_db)
383
-
384
- # 2. 如果关联了具体作品,更新作品详情的专属打赏榜与收益趋势
385
- if req.item_id:
386
- for item in items_db:
387
- if item["id"] == req.item_id:
388
- if "tip_history" not in item: item["tip_history"] = {}
389
- item["tip_history"][current_month] = item["tip_history"].get(current_month, 0) + req.amount
390
-
391
- if "tip_board" not in item: item["tip_board"] = []
392
- sender_entry = next((x for x in item["tip_board"] if x["account"] == req.sender_account), None)
393
- if sender_entry:
394
- sender_entry["amount"] += req.amount
395
- else:
396
- item["tip_board"].append({"account": req.sender_account, "amount": req.amount, "is_anon": req.is_anonymous})
397
- item["tip_board"] = sorted(item["tip_board"], key=lambda x: x["amount"], reverse=True)
398
- json_db.save_data("items.json", items_db)
399
- break
400
-
401
- return {"status": "success", "balance": sender_wallet.balance}
402
- except HTTPException:
403
- db.rollback()
404
- raise
405
- except Exception as e:
406
- db.rollback()
407
- logger.error(f"TIP_ERROR | from={req.sender_account} | to={req.target_account} | amount={req.amount} | error={str(e)}")
408
- raise HTTPException(status_code=500, detail="打赏处理失败,请稍后重试")
409
-
410
- @router.post("/api/wallet/withdraw")
411
- @limiter.limit("3/minute") # 🔒 P0安全优化:提现每分钟最多3次
412
- async def withdraw(request: Request, req: WithdrawRequest, db: Session = Depends(get_db)):
413
- key = f"{req.account}_withdraw"
414
- code_data = VERIFY_CODES.get(key)
415
- # 🔒 P0安全修复:统一使用 expires_at 字段,兼容旧版 expires
416
- expire_time = code_data.get("expires_at", code_data.get("expires", 0)) if code_data else 0
417
- if not code_data or code_data["code"] != req.code or time.time() > expire_time:
418
- raise HTTPException(status_code=400, detail="验证码无效或已过期")
419
-
420
- # 🔒 P1并发安全:使用悲观锁+异常处理+事务回滚防��并发问题
421
- try:
422
- wallet = db.query(Wallet).filter(Wallet.account == req.account).with_for_update().first()
423
- if not wallet:
424
- raise HTTPException(status_code=400, detail="钱包不存在")
425
-
426
- # 🚀 核心新增:阶梯手续费计算 (与前端逻辑统一)
427
- # 查询历史累计提现总额 (WITHDRAW 类型的 amount 是负数,需要取绝对值)
428
- # 🚀 P1性能优化:使用聚合函数代替 .all() + sum
429
- from sqlalchemy import func as sql_func
430
- withdrawals_sum = db.query(sql_func.coalesce(sql_func.sum(Transaction.amount), 0)).filter(
431
- Transaction.account == req.account,
432
- Transaction.tx_type == 'WITHDRAW'
433
- ).scalar() or 0
434
- total_withdrawn = abs(withdrawals_sum)
435
-
436
- # 手续费规则:100元 = 10000积分 免手续费额度,超出部分收取 10%
437
- free_quota = max(0, 10000 - total_withdrawn) # 剩余免责额度
438
- fee_amount = 0
439
- if req.amount > free_quota:
440
- fee_amount = int((req.amount - free_quota) * 0.10) # 只对超出部分收 10%
441
-
442
- actual_withdraw = req.amount # 从账户扣除的金额
443
- net_amount = req.amount - fee_amount # 用户实际到账金额
444
-
445
- total_withdrawable = wallet.earn_balance + wallet.tip_balance
446
- if actual_withdraw > total_withdrawable:
447
- raise HTTPException(status_code=400, detail="可提现余额不足")
448
-
449
- if actual_withdraw <= wallet.earn_balance:
450
- wallet.earn_balance -= actual_withdraw
451
- else:
452
- remaining = actual_withdraw - wallet.earn_balance
453
- wallet.earn_balance = 0
454
- wallet.tip_balance -= remaining
455
-
456
- wallet.frozen_balance += net_amount # 冻结的是到账金额,非手续费部分
457
-
458
- tx_id = f"WD_{int(time.time())}_{uuid.uuid4().hex[:6]}"
459
- last_tx = db.query(Transaction).filter(Transaction.account == req.account).order_by(Transaction.created_at.desc()).first()
460
- prev_hash = last_tx.tx_hash if last_tx else "GENESIS_HASH"
461
- tx_hash = calculate_tx_hash(tx_id, req.account, "WITHDRAW", -actual_withdraw, prev_hash)
462
-
463
- new_tx = Transaction(
464
- tx_id=tx_id, account=req.account, tx_type="WITHDRAW", amount=-actual_withdraw,
465
- prev_hash=prev_hash, tx_hash=tx_hash
466
- )
467
- db.add(new_tx)
468
-
469
- # 🚀 如果有手续费,额外记录一笔手续费交易
470
- if fee_amount > 0:
471
- fee_tx_id = f"FEE_{int(time.time())}_{uuid.uuid4().hex[:6]}"
472
- fee_tx_hash = calculate_tx_hash(fee_tx_id, req.account, "WITHDRAW_FEE", -fee_amount, tx_hash)
473
- fee_tx = Transaction(
474
- tx_id=fee_tx_id, account=req.account, tx_type="WITHDRAW_FEE", amount=-fee_amount,
475
- prev_hash=tx_hash, tx_hash=fee_tx_hash
476
- )
477
- db.add(fee_tx)
478
-
479
- db.commit()
480
-
481
- # 📝 P2优化:提现审计日志
482
- logger.info(f"WITHDRAW | account={req.account} | amount={actual_withdraw} | fee={fee_amount} | net={net_amount} | tx={tx_id}")
483
-
484
- del VERIFY_CODES[key]
485
- return {
486
- "status": "success",
487
- "withdraw_amount": actual_withdraw,
488
- "fee_amount": fee_amount,
489
- "net_amount": net_amount,
490
- "free_quota_used": min(req.amount, free_quota + total_withdrawn) - total_withdrawn # 本次消耗的免责额度
491
- }
492
- except HTTPException:
493
- db.rollback()
494
- raise
495
- except Exception as e:
496
- db.rollback()
497
- logger.error(f"WITHDRAW_ERROR | account={req.account} | amount={req.amount} | error={str(e)}")
498
- raise HTTPException(status_code=500, detail="提现处理失败,请稍后重试")
499
-
500
- # ==========================================
501
- # 💳 P6支付增强:交易明细查询API
502
- # ==========================================
503
-
504
- @router.get("/api/wallet/{account}/transactions")
505
- async def get_transactions(
506
- account: str,
507
- page: int = 1,
508
- limit: int = 20,
509
- tx_type: str = None,
510
- db: Session = Depends(get_db)
511
- ):
512
- """
513
- 获取用户交易明细(分页)
514
- - tx_type: 可选筛选(RECHARGE/PURCHASE/TIP_OUT/TIP_IN/WITHDRAW/TASK_FREEZE/TASK_DEPOSIT/TASK_PAYMENT/TASK_INCOME/TASK_REFUND)
515
- """
516
- query = db.query(Transaction).filter(Transaction.account == account)
517
-
518
- if tx_type:
519
- query = query.filter(Transaction.tx_type == tx_type)
520
-
521
- total = query.count()
522
- transactions = query.order_by(Transaction.created_at.desc()).offset((page - 1) * limit).limit(limit).all()
523
-
524
- # 格式化输出
525
- tx_list = []
526
- for tx in transactions:
527
- tx_list.append({
528
- "tx_id": tx.tx_id,
529
- "tx_type": tx.tx_type,
530
- "amount": tx.amount,
531
- "related_account": tx.related_account,
532
- "item_id": tx.item_id,
533
- "created_at": tx.created_at.isoformat() if tx.created_at else None
534
- })
535
-
536
- return {
537
- "status": "success",
538
- "data": tx_list,
539
- "total": total,
540
- "page": page,
541
- "limit": limit
542
- }
543
-
544
- @router.get("/api/wallet/{account}/task-stats")
545
- async def get_task_stats(account: str, db: Session = Depends(get_db)):
546
- """
547
- 📊 获取用户任务收益统计
548
- 🚀 P1性能优化:使单次查询+组聚合替代多次查询
549
- """
550
- from sqlalchemy import func as sql_func, case
551
-
552
- # 🚀 P1性能优化:使用分组聚合一次查询多种类型的统计
553
- stats = db.query(
554
- Transaction.tx_type,
555
- sql_func.count(Transaction.tx_id).label('count'),
556
- sql_func.coalesce(sql_func.sum(Transaction.amount), 0).label('total')
557
- ).filter(
558
- Transaction.account == account,
559
- Transaction.tx_type.in_(["TASK_INCOME", "TASK_FREEZE", "TASK_DEPOSIT", "TASK_PAYMENT", "TASK_REFUND"])
560
- ).group_by(Transaction.tx_type).all()
561
-
562
- # 解析统计结果
563
- stats_map = {s.tx_type: {'count': s.count, 'total': s.total} for s in stats}
564
-
565
- total_income = stats_map.get('TASK_INCOME', {}).get('total', 0) or 0
566
- income_count = stats_map.get('TASK_INCOME', {}).get('count', 0) or 0
567
-
568
- # 任务支出(发布任务的支付)
569
- total_payment = abs(
570
- (stats_map.get('TASK_FREEZE', {}).get('total', 0) or 0) +
571
- (stats_map.get('TASK_DEPOSIT', {}).get('total', 0) or 0) +
572
- (stats_map.get('TASK_PAYMENT', {}).get('total', 0) or 0)
573
- )
574
- payment_count = (
575
- (stats_map.get('TASK_FREEZE', {}).get('count', 0) or 0) +
576
- (stats_map.get('TASK_DEPOSIT', {}).get('count', 0) or 0) +
577
- (stats_map.get('TASK_PAYMENT', {}).get('count', 0) or 0)
578
- )
579
-
580
- total_refund = stats_map.get('TASK_REFUND', {}).get('total', 0) or 0
581
-
582
- # 最近交易(任务相关)
583
- recent_txs = db.query(Transaction).filter(
584
- Transaction.account == account,
585
- Transaction.tx_type.in_(["TASK_INCOME", "TASK_PAYMENT", "TASK_DEPOSIT", "TASK_FREEZE", "TASK_REFUND"])
586
- ).order_by(Transaction.created_at.desc()).limit(10).all()
587
-
588
- recent_list = [{
589
- "tx_id": tx.tx_id,
590
- "tx_type": tx.tx_type,
591
- "amount": tx.amount,
592
- "item_id": tx.item_id,
593
- "created_at": tx.created_at.isoformat() if tx.created_at else None
594
- } for tx in recent_txs]
595
-
596
- return {
597
- "status": "success",
598
- "data": {
599
- "total_income": total_income,
600
- "income_count": income_count,
601
- "total_payment": total_payment,
602
- "payment_count": payment_count,
603
- "total_refund": total_refund,
604
- "net_earnings": total_income - total_payment + total_refund,
605
- "recent_transactions": recent_list
606
- }
607
- }
608
-
609
- # ==========================================
610
- # 🔄 P7后悔模式:退款API
611
- # ==========================================
612
-
613
- @router.get("/api/wallet/{account}/purchase/{item_id}")
614
- async def get_purchase_status(account: str, item_id: str, db: Session = Depends(get_db)):
615
- """
616
- 获取购买状态(用于判断是否可退款)
617
- """
618
- ownership = db.query(Ownership).filter(
619
- Ownership.account == account,
620
- Ownership.item_id == item_id,
621
- Ownership.is_refunded == False
622
- ).first()
623
-
624
- if not ownership:
625
- return {"status": "success", "owned": False}
626
-
627
- # 计算是否在退款窗口内
628
- purchased_at = ownership.purchased_at
629
- now = datetime.datetime.utcnow()
630
- refund_deadline = purchased_at + datetime.timedelta(hours=REFUND_WINDOW_HOURS)
631
- can_refund = now < refund_deadline
632
- hours_left = max(0, (refund_deadline - now).total_seconds() / 3600) if can_refund else 0
633
-
634
- return {
635
- "status": "success",
636
- "owned": True,
637
- "purchased_at": purchased_at.isoformat(),
638
- "price_paid": ownership.price_paid,
639
- "can_refund": can_refund,
640
- "refund_hours_left": round(hours_left, 1)
641
- }
642
-
643
- @router.post("/api/wallet/refund")
644
- @limiter.limit("3/minute") # 🔒 P0安全优化:退款每分钟最多3次
645
- async def refund_purchase(request: Request, account: str, item_id: str, db: Session = Depends(get_db)):
646
- """
647
- 🔄 P7后悔模式:申请退款
648
- - 24小时内可退款
649
- - 退款后30天内禁止再次购买
650
- - 退款后权限回收
651
- """
652
- items_db = json_db.load_data("items.json", default_data=[])
653
- item = next((i for i in items_db if i["id"] == item_id), None)
654
-
655
- if not item:
656
- raise HTTPException(status_code=404, detail="商品不存在")
657
-
658
- # 查找购买记录
659
- ownership = db.query(Ownership).filter(
660
- Ownership.account == account,
661
- Ownership.item_id == item_id,
662
- Ownership.is_refunded == False
663
- ).first()
664
-
665
- if not ownership:
666
- raise HTTPException(status_code=404, detail="未找到购买记录")
667
-
668
- # 检查是否在退款窗口内
669
- purchased_at = ownership.purchased_at
670
- now = datetime.datetime.utcnow()
671
- refund_deadline = purchased_at + datetime.timedelta(hours=REFUND_WINDOW_HOURS)
672
-
673
- if now >= refund_deadline:
674
- hours_passed = (now - purchased_at).total_seconds() / 3600
675
- raise HTTPException(status_code=400, detail=f"已超过24小时退款窗口(已购买{hours_passed:.1f}小时)")
676
-
677
- refund_amount = ownership.price_paid or 0
678
- seller_account = item.get("author")
679
-
680
- if refund_amount <= 0:
681
- raise HTTPException(status_code=400, detail="该商品为免费资源,无需退款")
682
-
683
- # 🔒 P1并发安全:使用悲观锁+异常处理+事务回滚防止并发问题
684
- try:
685
- # 执行退款
686
- buyer_wallet = db.query(Wallet).filter(Wallet.account == account).with_for_update().first()
687
- seller_wallet = db.query(Wallet).filter(Wallet.account == seller_account).with_for_update().first()
688
-
689
- if seller_wallet:
690
- # 从卖家收益中扣除(如果不足则从余额扣除)
691
- if seller_wallet.earn_balance >= refund_amount:
692
- seller_wallet.earn_balance -= refund_amount
693
- else:
694
- remaining = refund_amount - seller_wallet.earn_balance
695
- seller_wallet.earn_balance = 0
696
- seller_wallet.balance = max(0, seller_wallet.balance - remaining)
697
-
698
- if buyer_wallet:
699
- buyer_wallet.balance += refund_amount
700
- else:
701
- buyer_wallet = Wallet(account=account, balance=refund_amount)
702
- db.add(buyer_wallet)
703
-
704
- # 标记所有权为已退款
705
- ownership.is_refunded = True
706
- ownership.refunded_at = now
707
-
708
- # 创建退款记录(30天禁购)
709
- ban_until = now + datetime.timedelta(days=REFUND_BAN_DAYS)
710
- new_refund = Refund(
711
- account=account,
712
- item_id=item_id,
713
- amount=refund_amount,
714
- ban_until=ban_until
715
- )
716
- db.add(new_refund)
717
-
718
- # 记录退款交易
719
- tx_id = f"REFUND_{int(time.time())}_{uuid.uuid4().hex[:6]}"
720
- last_tx = db.query(Transaction).filter(Transaction.account == account).order_by(Transaction.created_at.desc()).first()
721
- prev_hash = last_tx.tx_hash if last_tx else "GENESIS_HASH"
722
- tx_hash = calculate_tx_hash(tx_id, account, "REFUND", refund_amount, prev_hash)
723
-
724
- new_tx = Transaction(
725
- tx_id=tx_id, account=account, tx_type="REFUND", amount=refund_amount,
726
- related_account=seller_account, item_id=item_id, prev_hash=prev_hash, tx_hash=tx_hash
727
- )
728
- db.add(new_tx)
729
- db.commit()
730
-
731
- logger.info(f"REFUND | buyer={account} | seller={seller_account} | item={item_id} | amount={refund_amount} | ban_until={ban_until.isoformat()}")
732
-
733
- return {
734
- "status": "success",
735
- "message": f"退款成功,{refund_amount}积分已退还",
736
- "refund_amount": refund_amount,
737
- "ban_days": REFUND_BAN_DAYS
738
- }
739
- except HTTPException:
740
- db.rollback()
741
- raise
742
- except Exception as e:
743
- db.rollback()
744
- logger.error(f"REFUND_ERROR | buyer={account} | item={item_id} | amount={refund_amount} | error={str(e)}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
745
  raise HTTPException(status_code=500, detail="退款处理失败,请稍后重试")
 
1
+ # router_wallet.py
2
+ # ==========================================
3
+ # 💰 钱包与交易路由模块
4
+ # ==========================================
5
+ # 作用:处理充值、提现、购买、打赏等资金操作
6
+ # 关联文件:
7
+ # - verify_code_engine.py (提现验证码缓存)
8
+ # - database_sql.py (SQL数据库连接)
9
+ # - models_sql.py (Wallet, Transaction, Ownership 模型)
10
+ # 🔒 P0安全优化:API限流
11
+ # ==========================================
12
+
13
+ from fastapi import APIRouter, Depends, HTTPException, Request
14
+ from fastapi.responses import Response
15
+ from sqlalchemy.orm import Session
16
+ import time
17
+ import uuid
18
+ import hashlib
19
+ import os
20
+ import datetime
21
+ import logging
22
+ from database_sql import get_db
23
+ from models_sql import Wallet, Transaction, Ownership, Refund
24
+ from models import RechargeRequest, WithdrawRequest, PurchaseRequest, TipRequest
25
+ import 数据库连接 as json_db
26
+
27
+ # 🔒 P0安全优化:API限流
28
+ from slowapi import Limiter
29
+ from slowapi.util import get_remote_address
30
+ limiter = Limiter(key_func=get_remote_address)
31
+
32
+ # 🔄 P7后悔模式:24小时退款窗口
33
+ REFUND_WINDOW_HOURS = 24
34
+ # 🔄 P7后悔模式:退款后30天禁购
35
+ REFUND_BAN_DAYS = 30
36
+
37
+ # 📝 P2优化:审计日志
38
+ logger = logging.getLogger("ComfyUI-Ranking.Wallet")
39
+
40
+ # 🔐 导入验证码缓存 (提现时需要验证)
41
+ from verify_code_engine import VERIFY_CODES
42
+
43
+ router = APIRouter()
44
+
45
+ # ==========================================
46
+ # 🚨 替换这里的支付宝初始化逻辑 🚨
47
+ # ==========================================
48
+ def format_pem_key(key_str, key_type="PRIVATE"):
49
+ """自动给'裸奔'的秘钥穿上符合 Python 规范的外套"""
50
+ # 如果用户已经带有 BEGIN 标签,说明格式没问题,直接返回
51
+ if "BEGIN" in key_str:
52
+ return key_str.replace("\\n", "\n").strip()
53
+
54
+ # 去除所有的空格和换行符,拿到最纯净的字符串
55
+ clean_key = key_str.replace(" ", "").replace("\n", "").replace("\\n", "").replace("\r", "")
56
+
57
+ # 按照国际标准,每 64 个字符强制切断换行
58
+ chunks = [clean_key[i:i+64] for i in range(0, len(clean_key), 64)]
59
+ formatted_body = "\n".join(chunks)
60
+
61
+ # 穿上头尾外套
62
+ if key_type == "PRIVATE":
63
+ return f"-----BEGIN RSA PRIVATE KEY-----\n{formatted_body}\n-----END RSA PRIVATE KEY-----"
64
+ else:
65
+ return f"-----BEGIN PUBLIC KEY-----\n{formatted_body}\n-----END PUBLIC KEY-----"
66
+
67
+ alipay_error_msg = "未知错误"
68
+ try:
69
+ from alipay import AliPay
70
+ from alipay.utils import AliPayConfig
71
+
72
+ # 1. 抓取原生态的环境变量
73
+ raw_appid = os.environ.get("ALIPAY_APPID", "").strip()
74
+ raw_priv_key = os.environ.get("ALIPAY_PRIVATE_KEY", "").strip()
75
+ raw_pub_key = os.environ.get("ALIPAY_PUBLIC_KEY", "").strip()
76
+
77
+ if not raw_appid or not raw_priv_key or not raw_pub_key:
78
+ alipay_error_msg = f"缺少环境变量。当前读取到: APPID={bool(raw_appid)}, PRIV_KEY={bool(raw_priv_key)}, PUB_KEY={bool(raw_pub_key)}"
79
+ alipay = None
80
+ else:
81
+ # 2. 扔进我们的格式化引擎进行自动包装!
82
+ priv_key_formatted = format_pem_key(raw_priv_key, "PRIVATE")
83
+ pub_key_formatted = format_pem_key(raw_pub_key, "PUBLIC")
84
+
85
+ # 3. 完美加载
86
+ alipay = AliPay(
87
+ appid=raw_appid,
88
+ app_notify_url="https://zhiwei666-comfyui-ranking-api.hf.space/api/wallet/alipay_notify",
89
+ app_private_key_string=priv_key_formatted,
90
+ alipay_public_key_string=pub_key_formatted,
91
+ sign_type="RSA2",
92
+ debug=False,
93
+ config=AliPayConfig(timeout=15)
94
+ )
95
+ except Exception as e:
96
+ alipay = None
97
+ alipay_error_msg = f"支付宝 SDK 崩溃: {str(e)}"
98
+ print(f"🚨 支付宝初始化异常: {alipay_error_msg}")
99
+
100
+ def calculate_tx_hash(tx_id, account, tx_type, amount, prev_hash):
101
+ data = f"{tx_id}{account}{tx_type}{amount}{prev_hash}"
102
+ return hashlib.sha256(data.encode()).hexdigest()
103
+
104
+ @router.post("/api/wallet/create_recharge_order")
105
+ async def create_recharge_order(req: RechargeRequest):
106
+ if not alipay:
107
+ # 这里会将真实的错误原因直接弹窗发给前端!
108
+ raise HTTPException(status_code=500, detail=f"支付网关配置错误: {alipay_error_msg}")
109
+
110
+ order_id = f"PAY_{int(time.time())}_{uuid.uuid4().hex[:6]}"
111
+ subject = f"ComfyUI Community Points - {req.account}"
112
+
113
+ order_string = alipay.api_alipay_trade_precreate(
114
+ out_trade_no=order_id,
115
+ total_amount=str(req.amount),
116
+ subject=subject
117
+ )
118
+
119
+ qr_code_url = order_string.get("qr_code")
120
+ if not qr_code_url:
121
+ raise HTTPException(status_code=500, detail="生成支付二维码失败")
122
+
123
+ return {"status": "success", "order_id": order_id, "qr_code": qr_code_url}
124
+
125
+ # 🟢 业务流转细节修复:正确解析 application/x-www-form-urlencoded
126
+ @router.post("/api/wallet/alipay_notify")
127
+ async def alipay_notify(request: Request, db: Session = Depends(get_db)):
128
+ # 强制将表单数据解析为纯字典,防止由于数据类型错误导致验签失败
129
+ form_data = await request.form()
130
+ data = dict(form_data.items())
131
+
132
+ signature = data.pop("sign", None)
133
+ data.pop("sign_type", None)
134
+
135
+ if not alipay or not signature or not alipay.verify(data, signature):
136
+ return Response(content="fail", media_type="text/plain")
137
+
138
+ if data.get("trade_status") in ("TRADE_SUCCESS", "TRADE_FINISHED"):
139
+ order_id = data.get("out_trade_no")
140
+
141
+ existing_tx = db.query(Transaction).filter(Transaction.tx_id == order_id).first()
142
+ if not existing_tx:
143
+ amount = int(float(data.get("total_amount", 0)))
144
+ account = data.get("subject", "").split(" - ")[-1]
145
+
146
+ # 🔒 P1并发安全:使用悲观锁+异常处理+事务回滚防止并发充值问题
147
+ try:
148
+ wallet = db.query(Wallet).filter(Wallet.account == account).with_for_update().first()
149
+ if not wallet:
150
+ wallet = Wallet(account=account)
151
+ db.add(wallet)
152
+
153
+ wallet.balance += amount
154
+
155
+ last_tx = db.query(Transaction).filter(Transaction.account == account).order_by(Transaction.created_at.desc()).first()
156
+ prev_hash = last_tx.tx_hash if last_tx else "GENESIS_HASH"
157
+ tx_hash = calculate_tx_hash(order_id, account, "RECHARGE", amount, prev_hash)
158
+
159
+ new_tx = Transaction(
160
+ tx_id=order_id, account=account, tx_type="RECHARGE", amount=amount,
161
+ prev_hash=prev_hash, tx_hash=tx_hash
162
+ )
163
+ db.add(new_tx)
164
+ db.commit()
165
+
166
+ # 📝 P2优化:充值审计日志
167
+ logger.info(f"RECHARGE | account={account} | amount={amount} | order={order_id}")
168
+ except Exception as e:
169
+ db.rollback()
170
+ logger.error(f"RECHARGE_ERROR | account={account} | amount={amount} | order={order_id} | error={str(e)}")
171
+ return Response(content="fail", media_type="text/plain")
172
+
173
+ return Response(content="success", media_type="text/plain")
174
+
175
+ @router.get("/api/wallet/check_order/{order_id}")
176
+ async def check_order(order_id: str, account: str = None, db: Session = Depends(get_db)):
177
+ # 防线 1:先查本地数据库(如果 Webhook 成功了,这里就能查到)
178
+ tx = db.query(Transaction).filter(Transaction.tx_id == order_id).first()
179
+ if tx:
180
+ return {"status": "SUCCESS"}
181
+
182
+ # 防线 2:主动向支付宝发起查单(解决 Hugging Face 收不到回调的绝杀技)
183
+ if alipay and account:
184
+ try:
185
+ # 拿着订单号去问支付宝:这笔钱到底付了没?
186
+ result = alipay.api_alipay_trade_query(out_trade_no=order_id)
187
+ if result.get("code") == "10000" and result.get("trade_status") in ("TRADE_SUCCESS", "TRADE_FINISHED"):
188
+
189
+ # 钱真的到了!立刻加锁入账
190
+ amount = int(float(result.get("total_amount", 0)))
191
+
192
+ wallet = db.query(Wallet).filter(Wallet.account == account).with_for_update().first()
193
+ if not wallet:
194
+ wallet = Wallet(account=account)
195
+ db.add(wallet)
196
+
197
+ wallet.balance += amount
198
+
199
+ last_tx = db.query(Transaction).filter(Transaction.account == account).order_by(Transaction.created_at.desc()).first()
200
+ prev_hash = last_tx.tx_hash if last_tx else "GENESIS_HASH"
201
+ tx_hash = calculate_tx_hash(order_id, account, "RECHARGE", amount, prev_hash)
202
+
203
+ new_tx = Transaction(
204
+ tx_id=order_id, account=account, tx_type="RECHARGE", amount=amount,
205
+ prev_hash=prev_hash, tx_hash=tx_hash
206
+ )
207
+ db.add(new_tx)
208
+ db.commit()
209
+
210
+ return {"status": "SUCCESS"}
211
+ except Exception as e:
212
+ print(f"主动查单发生异常: {e}")
213
+
214
+ # 如果没查到支付成功状态,继续让前端等
215
+ return {"status": "PENDING"}
216
+
217
+ @router.get("/api/wallet/{account}")
218
+ async def get_wallet(account: str, db: Session = Depends(get_db)):
219
+ wallet = db.query(Wallet).filter(Wallet.account == account).first()
220
+
221
+ # 🚀 P1性能优化:使用聚合函数代替 .all() + sum
222
+ from sqlalchemy import func
223
+ total_withdrawn = db.query(func.coalesce(func.sum(Transaction.amount), 0)).filter(
224
+ Transaction.account == account,
225
+ Transaction.tx_type == 'WITHDRAW'
226
+ ).scalar() or 0
227
+ total_withdrawn = abs(total_withdrawn) # 提现金额是负数
228
+
229
+ if not wallet:
230
+ return {"status": "success", "balance": 0, "earn_balance": 0, "tip_balance": 0, "frozen_balance": 0, "total_withdrawn": total_withdrawn}
231
+
232
+ return {
233
+ "status": "success",
234
+ "balance": wallet.balance,
235
+ "earn_balance": wallet.earn_balance,
236
+ "tip_balance": wallet.tip_balance,
237
+ "frozen_balance": wallet.frozen_balance,
238
+ "total_withdrawn": total_withdrawn # 暴露给前端
239
+ }
240
+
241
+ @router.post("/api/wallet/purchase")
242
+ @limiter.limit("10/minute") # 🔒 P0安全优化:购买每分钟最多10次
243
+ async def purchase_item(request: Request, req: PurchaseRequest, db: Session = Depends(get_db)):
244
+ items_db = json_db.load_data("items.json", default_data=[])
245
+ item = next((i for i in items_db if i["id"] == req.item_id), None)
246
+
247
+ if not item:
248
+ raise HTTPException(status_code=404, detail="商品不存在")
249
+
250
+ # 🔄 P7后悔模式:检查价格是否延迟生效
251
+ actual_price = item.get("price", 0)
252
+ pending_price = item.get("pending_price")
253
+ pending_price_effective = item.get("pending_price_effective_at")
254
+ if pending_price is not None and pending_price_effective:
255
+ # 检查是否已过生效时间
256
+ effective_time = datetime.datetime.fromisoformat(pending_price_effective)
257
+ if datetime.datetime.now() >= effective_time:
258
+ actual_price = pending_price
259
+ # 更新实际价格清除待生效价格
260
+ item["price"] = pending_price
261
+ item["pending_price"] = None
262
+ item["pending_price_effective_at"] = None
263
+ json_db.save_data("items.json", items_db)
264
+
265
+ price = int(actual_price)
266
+ seller_account = item.get("author")
267
+
268
+ if price <= 0 or req.account == seller_account:
269
+ # ☁️ 免费资源或作者本人,也返回网盘密码
270
+ return {
271
+ "status": "success",
272
+ "already_owned": True,
273
+ "netdisk_password": item.get("netdisk_password"), # ☁️
274
+ "is_netdisk": item.get("is_netdisk", False) # ☁️
275
+ }
276
+
277
+ # 🔄 P7后悔模式:检查30天禁购
278
+ refund_ban = db.query(Refund).filter(
279
+ Refund.account == req.account,
280
+ Refund.item_id == req.item_id,
281
+ Refund.ban_until > datetime.datetime.utcnow()
282
+ ).first()
283
+ if refund_ban:
284
+ days_left = (refund_ban.ban_until - datetime.datetime.utcnow()).days + 1
285
+ raise HTTPException(status_code=403, detail=f"您已退款过此商品,{days_left}天内禁止再次购买")
286
+
287
+ # 检查是否已拥有(排除已退款的记录)
288
+ owned = db.query(Ownership).filter(
289
+ Ownership.account == req.account,
290
+ Ownership.item_id == req.item_id,
291
+ Ownership.is_refunded == False
292
+ ).first()
293
+ if owned:
294
+ # ☁️ 已购买用户,返回网盘密码
295
+ return {
296
+ "status": "success",
297
+ "already_owned": True,
298
+ "netdisk_password": item.get("netdisk_password"), # ☁️
299
+ "is_netdisk": item.get("is_netdisk", False) # ☁️
300
+ }
301
+
302
+ # 🔒 P1并发安全:使用悲观锁+异常处理+事务回滚防止双花问题
303
+ try:
304
+ buyer_wallet = db.query(Wallet).filter(Wallet.account == req.account).with_for_update().first()
305
+ if not buyer_wallet or buyer_wallet.balance < price:
306
+ raise HTTPException(status_code=402, detail="余额不足,请先充值")
307
+
308
+ seller_wallet = db.query(Wallet).filter(Wallet.account == seller_account).with_for_update().first()
309
+ if not seller_wallet:
310
+ seller_wallet = Wallet(account=seller_account)
311
+ db.add(seller_wallet)
312
+
313
+ buyer_wallet.balance -= price
314
+ seller_wallet.earn_balance += price
315
+
316
+ # 🔄 P7后悔模式:记录购买价格
317
+ new_ownership = Ownership(account=req.account, item_id=req.item_id, price_paid=price)
318
+ db.add(new_ownership)
319
+
320
+ tx_id = f"BUY_{int(time.time())}_{uuid.uuid4().hex[:6]}"
321
+ last_tx = db.query(Transaction).filter(Transaction.account == req.account).order_by(Transaction.created_at.desc()).first()
322
+ prev_hash = last_tx.tx_hash if last_tx else "GENESIS_HASH"
323
+ tx_hash = calculate_tx_hash(tx_id, req.account, "PURCHASE", -price, prev_hash)
324
+
325
+ # 创建交易记录 (字段名为 related_account,与 models_sql.py 中 Transaction 模型保持一致)
326
+ new_tx = Transaction(
327
+ tx_id=tx_id, account=req.account, tx_type="PURCHASE", amount=-price,
328
+ related_account=seller_account, item_id=req.item_id, prev_hash=prev_hash, tx_hash=tx_hash
329
+ )
330
+ db.add(new_tx)
331
+ db.commit()
332
+
333
+ # 📝 P2优化:购买审计日志
334
+ logger.info(f"PURCHASE | buyer={req.account} | seller={seller_account} | item={req.item_id} | amount={price} | tx={tx_id}")
335
+
336
+ # ☁️ 购买成功后返回网盘密码
337
+ return {
338
+ "status": "success",
339
+ "already_owned": False,
340
+ "netdisk_password": item.get("netdisk_password"), # ☁️ 只有购买成功才返回
341
+ "is_netdisk": item.get("is_netdisk", False) # ☁️
342
+ }
343
+ except HTTPException:
344
+ db.rollback()
345
+ raise
346
+ except Exception as e:
347
+ db.rollback()
348
+ logger.error(f"PURCHASE_ERROR | buyer={req.account} | item={req.item_id} | error={str(e)}")
349
+ raise HTTPException(status_code=500, detail="购买处理失败,请稍后重试")
350
+
351
+ @router.post("/api/wallet/tip")
352
+ @limiter.limit("20/minute") # 🔒 P0安全优化:打赏每分钟最多20次
353
+ async def tip_user(request: Request, req: TipRequest, db: Session = Depends(get_db)):
354
+ if req.amount <= 0:
355
+ raise HTTPException(status_code=400, detail="打赏金额必须大于0")
356
+ if req.sender_account == req.target_account:
357
+ raise HTTPException(status_code=400, detail="不能打赏给自己")
358
+
359
+ # 🔒 P1并发安全:使用悲观锁+异常处理+事务回滚防止双花问题
360
+ try:
361
+ sender_wallet = db.query(Wallet).filter(Wallet.account == req.sender_account).with_for_update().first()
362
+ target_wallet = db.query(Wallet).filter(Wallet.account == req.target_account).with_for_update().first()
363
+
364
+ if not sender_wallet or sender_wallet.balance < req.amount:
365
+ raise HTTPException(status_code=400, detail="余额不足")
366
+ if not target_wallet:
367
+ target_wallet = Wallet(account=req.target_account, balance=0, earn_balance=0, tip_balance=0, frozen_balance=0)
368
+ db.add(target_wallet)
369
+
370
+ sender_wallet.balance -= req.amount
371
+ target_wallet.tip_balance += req.amount
372
+
373
+ tx_id_sender = f"TIP_OUT_{int(time.time())}_{uuid.uuid4().hex[:6]}"
374
+ tx_id_target = f"TIP_IN_{int(time.time())}_{uuid.uuid4().hex[:6]}"
375
+
376
+ # 记录交易
377
+ last_tx_sender = db.query(Transaction).filter(Transaction.account == req.sender_account).order_by(Transaction.created_at.desc()).first()
378
+ last_tx_target = db.query(Transaction).filter(Transaction.account == req.target_account).order_by(Transaction.created_at.desc()).first()
379
+ prev_hash_sender = last_tx_sender.tx_hash if last_tx_sender else "GENESIS_HASH"
380
+ prev_hash_target = last_tx_target.tx_hash if last_tx_target else "GENESIS_HASH"
381
+
382
+ # 发送方交易记录 (字段名为 related_account,与 models_sql.py 中 Transaction 模型保持一致)
383
+ tx_sender = Transaction(tx_id=tx_id_sender, account=req.sender_account, tx_type="TIP_OUT", amount=-req.amount,
384
+ related_account=req.target_account, prev_hash=prev_hash_sender,
385
+ tx_hash=calculate_tx_hash(tx_id_sender, req.sender_account, "TIP_OUT", -req.amount, prev_hash_sender))
386
+
387
+ # 接收方交易记录
388
+ tx_target = Transaction(tx_id=tx_id_target, account=req.target_account, tx_type="TIP_IN", amount=req.amount,
389
+ related_account=req.sender_account, prev_hash=prev_hash_target,
390
+ tx_hash=calculate_tx_hash(tx_id_target, req.target_account, "TIP_IN", req.amount, prev_hash_target))
391
+
392
+ db.add(tx_sender)
393
+ db.add(tx_target)
394
+ db.commit()
395
+
396
+ # 📝 P2优化:打赏审计日志
397
+ logger.info(f"TIP | from={req.sender_account} | to={req.target_account} | amount={req.amount} | item={req.item_id or 'N/A'} | anon={req.is_anonymous}")
398
+
399
+ # 🚀 核心新增:记录打赏榜单和月度收益趋势 (写入 JSON 以供高频读取)
400
+ users_db = json_db.load_data("users.json", default_data={})
401
+ items_db = json_db.load_data("items.json", default_data=[])
402
+ current_month = datetime.date.today().strftime("%Y-%m")
403
+
404
+ # 1. 更新创作者的总打赏榜与收益趋势
405
+ if req.target_account in users_db:
406
+ u = users_db[req.target_account]
407
+ if "tip_history" not in u: u["tip_history"] = {}
408
+ u["tip_history"][current_month] = u["tip_history"].get(current_month, 0) + req.amount
409
+
410
+ if "tip_board" not in u: u["tip_board"] = []
411
+ sender_entry = next((x for x in u["tip_board"] if x["account"] == req.sender_account), None)
412
+ if sender_entry:
413
+ sender_entry["amount"] += req.amount
414
+ else:
415
+ u["tip_board"].append({"account": req.sender_account, "amount": req.amount, "is_anon": req.is_anonymous})
416
+ u["tip_board"] = sorted(u["tip_board"], key=lambda x: x["amount"], reverse=True)
417
+ json_db.save_data("users.json", users_db)
418
+
419
+ # 2. 如果关联了具体作品,更新作品详情的专属打赏榜与收益趋势
420
+ if req.item_id:
421
+ for item in items_db:
422
+ if item["id"] == req.item_id:
423
+ if "tip_history" not in item: item["tip_history"] = {}
424
+ item["tip_history"][current_month] = item["tip_history"].get(current_month, 0) + req.amount
425
+
426
+ if "tip_board" not in item: item["tip_board"] = []
427
+ sender_entry = next((x for x in item["tip_board"] if x["account"] == req.sender_account), None)
428
+ if sender_entry:
429
+ sender_entry["amount"] += req.amount
430
+ else:
431
+ item["tip_board"].append({"account": req.sender_account, "amount": req.amount, "is_anon": req.is_anonymous})
432
+ item["tip_board"] = sorted(item["tip_board"], key=lambda x: x["amount"], reverse=True)
433
+ json_db.save_data("items.json", items_db)
434
+ break
435
+
436
+ return {"status": "success", "balance": sender_wallet.balance}
437
+ except HTTPException:
438
+ db.rollback()
439
+ raise
440
+ except Exception as e:
441
+ db.rollback()
442
+ logger.error(f"TIP_ERROR | from={req.sender_account} | to={req.target_account} | amount={req.amount} | error={str(e)}")
443
+ raise HTTPException(status_code=500, detail="打赏处理失败,请稍后重试")
444
+
445
+ @router.post("/api/wallet/withdraw")
446
+ @limiter.limit("3/minute") # 🔒 P0安全优化:提现每分钟最多3次
447
+ async def withdraw(request: Request, req: WithdrawRequest, db: Session = Depends(get_db)):
448
+ key = f"{req.account}_withdraw"
449
+ code_data = VERIFY_CODES.get(key)
450
+ # 🔒 P0安全修复:统一使用 expires_at 字段,兼容旧版 expires
451
+ expire_time = code_data.get("expires_at", code_data.get("expires", 0)) if code_data else 0
452
+ if not code_data or code_data["code"] != req.code or time.time() > expire_time:
453
+ raise HTTPException(status_code=400, detail="验证码无效或已过期")
454
+
455
+ # 🔒 P1并发安全:使用悲观锁+异常处理+事务回滚防止并发问题
456
+ try:
457
+ wallet = db.query(Wallet).filter(Wallet.account == req.account).with_for_update().first()
458
+ if not wallet:
459
+ raise HTTPException(status_code=400, detail="钱包不存在")
460
+
461
+ # 🚀 核心新增:阶梯手续费计算 (与前端逻辑统一)
462
+ # 查询历史累计提现总额 (WITHDRAW 类型的 amount 是负数,需要取绝对值)
463
+ # 🚀 P1性能优化:使用聚合函数代替 .all() + sum
464
+ from sqlalchemy import func as sql_func
465
+ withdrawals_sum = db.query(sql_func.coalesce(sql_func.sum(Transaction.amount), 0)).filter(
466
+ Transaction.account == req.account,
467
+ Transaction.tx_type == 'WITHDRAW'
468
+ ).scalar() or 0
469
+ total_withdrawn = abs(withdrawals_sum)
470
+
471
+ # 手续费规则:100元 = 10000积分 免手续费额度,超出部分收取 10%
472
+ free_quota = max(0, 10000 - total_withdrawn) # 剩余免责额度
473
+ fee_amount = 0
474
+ if req.amount > free_quota:
475
+ fee_amount = int((req.amount - free_quota) * 0.10) # 只对超出部分收 10%
476
+
477
+ actual_withdraw = req.amount # 从账户扣除的金额
478
+ net_amount = req.amount - fee_amount # 用户实际到账金额
479
+
480
+ total_withdrawable = wallet.earn_balance + wallet.tip_balance
481
+ if actual_withdraw > total_withdrawable:
482
+ raise HTTPException(status_code=400, detail="可提现余额不足")
483
+
484
+ if actual_withdraw <= wallet.earn_balance:
485
+ wallet.earn_balance -= actual_withdraw
486
+ else:
487
+ remaining = actual_withdraw - wallet.earn_balance
488
+ wallet.earn_balance = 0
489
+ wallet.tip_balance -= remaining
490
+
491
+ wallet.frozen_balance += net_amount # 冻结的是到账金额,非手续费部分
492
+
493
+ tx_id = f"WD_{int(time.time())}_{uuid.uuid4().hex[:6]}"
494
+ last_tx = db.query(Transaction).filter(Transaction.account == req.account).order_by(Transaction.created_at.desc()).first()
495
+ prev_hash = last_tx.tx_hash if last_tx else "GENESIS_HASH"
496
+ tx_hash = calculate_tx_hash(tx_id, req.account, "WITHDRAW", -actual_withdraw, prev_hash)
497
+
498
+ new_tx = Transaction(
499
+ tx_id=tx_id, account=req.account, tx_type="WITHDRAW", amount=-actual_withdraw,
500
+ prev_hash=prev_hash, tx_hash=tx_hash
501
+ )
502
+ db.add(new_tx)
503
+
504
+ # 🚀 如果有手续费,额外记录一笔手续费交易
505
+ if fee_amount > 0:
506
+ fee_tx_id = f"FEE_{int(time.time())}_{uuid.uuid4().hex[:6]}"
507
+ fee_tx_hash = calculate_tx_hash(fee_tx_id, req.account, "WITHDRAW_FEE", -fee_amount, tx_hash)
508
+ fee_tx = Transaction(
509
+ tx_id=fee_tx_id, account=req.account, tx_type="WITHDRAW_FEE", amount=-fee_amount,
510
+ prev_hash=tx_hash, tx_hash=fee_tx_hash
511
+ )
512
+ db.add(fee_tx)
513
+
514
+ db.commit()
515
+
516
+ # 📝 P2优化:提现审计日志
517
+ logger.info(f"WITHDRAW | account={req.account} | amount={actual_withdraw} | fee={fee_amount} | net={net_amount} | tx={tx_id}")
518
+
519
+ del VERIFY_CODES[key]
520
+ return {
521
+ "status": "success",
522
+ "withdraw_amount": actual_withdraw,
523
+ "fee_amount": fee_amount,
524
+ "net_amount": net_amount,
525
+ "free_quota_used": min(req.amount, free_quota + total_withdrawn) - total_withdrawn # 本次消耗的免责额度
526
+ }
527
+ except HTTPException:
528
+ db.rollback()
529
+ raise
530
+ except Exception as e:
531
+ db.rollback()
532
+ logger.error(f"WITHDRAW_ERROR | account={req.account} | amount={req.amount} | error={str(e)}")
533
+ raise HTTPException(status_code=500, detail="提现处理失败,请稍后重试")
534
+
535
+ # ==========================================
536
+ # 💳 P6支付增强:交易明细查询API
537
+ # ==========================================
538
+
539
+ @router.get("/api/wallet/{account}/transactions")
540
+ async def get_transactions(
541
+ account: str,
542
+ page: int = 1,
543
+ limit: int = 20,
544
+ tx_type: str = None,
545
+ db: Session = Depends(get_db)
546
+ ):
547
+ """
548
+ 获取户交易明细(页)
549
+ - tx_type: 可选筛选(RECHARGE/PURCHASE/TIP_OUT/TIP_IN/WITHDRAW/TASK_FREEZE/TASK_DEPOSIT/TASK_PAYMENT/TASK_INCOME/TASK_REFUND)
550
+ """
551
+ query = db.query(Transaction).filter(Transaction.account == account)
552
+
553
+ if tx_type:
554
+ query = query.filter(Transaction.tx_type == tx_type)
555
+
556
+ total = query.count()
557
+ transactions = query.order_by(Transaction.created_at.desc()).offset((page - 1) * limit).limit(limit).all()
558
+
559
+ # 格式化输出
560
+ tx_list = []
561
+ for tx in transactions:
562
+ tx_list.append({
563
+ "tx_id": tx.tx_id,
564
+ "tx_type": tx.tx_type,
565
+ "amount": tx.amount,
566
+ "related_account": tx.related_account,
567
+ "item_id": tx.item_id,
568
+ "created_at": tx.created_at.isoformat() if tx.created_at else None
569
+ })
570
+
571
+ return {
572
+ "status": "success",
573
+ "data": tx_list,
574
+ "total": total,
575
+ "page": page,
576
+ "limit": limit
577
+ }
578
+
579
+ @router.get("/api/wallet/{account}/task-stats")
580
+ async def get_task_stats(account: str, db: Session = Depends(get_db)):
581
+ """
582
+ 📊 获取用户任务收益统计
583
+ 🚀 P1性能优化:使用单次查询+分组聚合替代多次查询
584
+ """
585
+ from sqlalchemy import func as sql_func, case
586
+
587
+ # 🚀 P1性能优化:使用分组聚合一次查询多种类型的统计
588
+ stats = db.query(
589
+ Transaction.tx_type,
590
+ sql_func.count(Transaction.tx_id).label('count'),
591
+ sql_func.coalesce(sql_func.sum(Transaction.amount), 0).label('total')
592
+ ).filter(
593
+ Transaction.account == account,
594
+ Transaction.tx_type.in_(["TASK_INCOME", "TASK_FREEZE", "TASK_DEPOSIT", "TASK_PAYMENT", "TASK_REFUND"])
595
+ ).group_by(Transaction.tx_type).all()
596
+
597
+ # 解析统计结果
598
+ stats_map = {s.tx_type: {'count': s.count, 'total': s.total} for s in stats}
599
+
600
+ total_income = stats_map.get('TASK_INCOME', {}).get('total', 0) or 0
601
+ income_count = stats_map.get('TASK_INCOME', {}).get('count', 0) or 0
602
+
603
+ # 任务支出(发布任务的支付)
604
+ total_payment = abs(
605
+ (stats_map.get('TASK_FREEZE', {}).get('total', 0) or 0) +
606
+ (stats_map.get('TASK_DEPOSIT', {}).get('total', 0) or 0) +
607
+ (stats_map.get('TASK_PAYMENT', {}).get('total', 0) or 0)
608
+ )
609
+ payment_count = (
610
+ (stats_map.get('TASK_FREEZE', {}).get('count', 0) or 0) +
611
+ (stats_map.get('TASK_DEPOSIT', {}).get('count', 0) or 0) +
612
+ (stats_map.get('TASK_PAYMENT', {}).get('count', 0) or 0)
613
+ )
614
+
615
+ total_refund = stats_map.get('TASK_REFUND', {}).get('total', 0) or 0
616
+
617
+ # 最近交易(任务相关)
618
+ recent_txs = db.query(Transaction).filter(
619
+ Transaction.account == account,
620
+ Transaction.tx_type.in_(["TASK_INCOME", "TASK_PAYMENT", "TASK_DEPOSIT", "TASK_FREEZE", "TASK_REFUND"])
621
+ ).order_by(Transaction.created_at.desc()).limit(10).all()
622
+
623
+ recent_list = [{
624
+ "tx_id": tx.tx_id,
625
+ "tx_type": tx.tx_type,
626
+ "amount": tx.amount,
627
+ "item_id": tx.item_id,
628
+ "created_at": tx.created_at.isoformat() if tx.created_at else None
629
+ } for tx in recent_txs]
630
+
631
+ return {
632
+ "status": "success",
633
+ "data": {
634
+ "total_income": total_income,
635
+ "income_count": income_count,
636
+ "total_payment": total_payment,
637
+ "payment_count": payment_count,
638
+ "total_refund": total_refund,
639
+ "net_earnings": total_income - total_payment + total_refund,
640
+ "recent_transactions": recent_list
641
+ }
642
+ }
643
+
644
+ # ==========================================
645
+ # 🔄 P7后悔模式:退款API
646
+ # ==========================================
647
+
648
+ @router.get("/api/wallet/{account}/purchase/{item_id}")
649
+ async def get_purchase_status(account: str, item_id: str, db: Session = Depends(get_db)):
650
+ """
651
+ 获取购买状态(用于判断是否可退款)
652
+ """
653
+ ownership = db.query(Ownership).filter(
654
+ Ownership.account == account,
655
+ Ownership.item_id == item_id,
656
+ Ownership.is_refunded == False
657
+ ).first()
658
+
659
+ if not ownership:
660
+ return {"status": "success", "owned": False}
661
+
662
+ # 计算是否在退款窗口内
663
+ purchased_at = ownership.purchased_at
664
+ now = datetime.datetime.utcnow()
665
+ refund_deadline = purchased_at + datetime.timedelta(hours=REFUND_WINDOW_HOURS)
666
+ can_refund = now < refund_deadline
667
+ hours_left = max(0, (refund_deadline - now).total_seconds() / 3600) if can_refund else 0
668
+
669
+ return {
670
+ "status": "success",
671
+ "owned": True,
672
+ "purchased_at": purchased_at.isoformat(),
673
+ "price_paid": ownership.price_paid,
674
+ "can_refund": can_refund,
675
+ "refund_hours_left": round(hours_left, 1)
676
+ }
677
+
678
+ @router.post("/api/wallet/refund")
679
+ @limiter.limit("3/minute") # 🔒 P0安全优化:退款每分钟最多3次
680
+ async def refund_purchase(request: Request, account: str, item_id: str, db: Session = Depends(get_db)):
681
+ """
682
+ 🔄 P7后悔模式:申请退款
683
+ - 24小时内可退款
684
+ - 退款后30天内禁止再次购买
685
+ - 退款后权限回收
686
+ """
687
+ items_db = json_db.load_data("items.json", default_data=[])
688
+ item = next((i for i in items_db if i["id"] == item_id), None)
689
+
690
+ if not item:
691
+ raise HTTPException(status_code=404, detail="商品不存在")
692
+
693
+ # 查找购买记录
694
+ ownership = db.query(Ownership).filter(
695
+ Ownership.account == account,
696
+ Ownership.item_id == item_id,
697
+ Ownership.is_refunded == False
698
+ ).first()
699
+
700
+ if not ownership:
701
+ raise HTTPException(status_code=404, detail="未找到购买记录")
702
+
703
+ # 检查是否在退款窗口内
704
+ purchased_at = ownership.purchased_at
705
+ now = datetime.datetime.utcnow()
706
+ refund_deadline = purchased_at + datetime.timedelta(hours=REFUND_WINDOW_HOURS)
707
+
708
+ if now >= refund_deadline:
709
+ hours_passed = (now - purchased_at).total_seconds() / 3600
710
+ raise HTTPException(status_code=400, detail=f"已超过24小时退款窗口(已购买{hours_passed:.1f}小时)")
711
+
712
+ refund_amount = ownership.price_paid or 0
713
+ seller_account = item.get("author")
714
+
715
+ if refund_amount <= 0:
716
+ raise HTTPException(status_code=400, detail="该商品为免费资源,无需退款")
717
+
718
+ # 🔒 P1并发安全:使用悲观锁+异常处理+事务回滚防止并发问题
719
+ try:
720
+ # 执行退款
721
+ buyer_wallet = db.query(Wallet).filter(Wallet.account == account).with_for_update().first()
722
+ seller_wallet = db.query(Wallet).filter(Wallet.account == seller_account).with_for_update().first()
723
+
724
+ if seller_wallet:
725
+ # 从卖家收益中扣除(如果不足则从余额扣除)
726
+ if seller_wallet.earn_balance >= refund_amount:
727
+ seller_wallet.earn_balance -= refund_amount
728
+ else:
729
+ remaining = refund_amount - seller_wallet.earn_balance
730
+ seller_wallet.earn_balance = 0
731
+ seller_wallet.balance = max(0, seller_wallet.balance - remaining)
732
+
733
+ if buyer_wallet:
734
+ buyer_wallet.balance += refund_amount
735
+ else:
736
+ buyer_wallet = Wallet(account=account, balance=refund_amount)
737
+ db.add(buyer_wallet)
738
+
739
+ # 标记所有权为已退款
740
+ ownership.is_refunded = True
741
+ ownership.refunded_at = now
742
+
743
+ # 创建退款记录(30天禁购)
744
+ ban_until = now + datetime.timedelta(days=REFUND_BAN_DAYS)
745
+ new_refund = Refund(
746
+ account=account,
747
+ item_id=item_id,
748
+ amount=refund_amount,
749
+ ban_until=ban_until
750
+ )
751
+ db.add(new_refund)
752
+
753
+ # 记录退款交易
754
+ tx_id = f"REFUND_{int(time.time())}_{uuid.uuid4().hex[:6]}"
755
+ last_tx = db.query(Transaction).filter(Transaction.account == account).order_by(Transaction.created_at.desc()).first()
756
+ prev_hash = last_tx.tx_hash if last_tx else "GENESIS_HASH"
757
+ tx_hash = calculate_tx_hash(tx_id, account, "REFUND", refund_amount, prev_hash)
758
+
759
+ new_tx = Transaction(
760
+ tx_id=tx_id, account=account, tx_type="REFUND", amount=refund_amount,
761
+ related_account=seller_account, item_id=item_id, prev_hash=prev_hash, tx_hash=tx_hash
762
+ )
763
+ db.add(new_tx)
764
+ db.commit()
765
+
766
+ logger.info(f"REFUND | buyer={account} | seller={seller_account} | item={item_id} | amount={refund_amount} | ban_until={ban_until.isoformat()}")
767
+
768
+ return {
769
+ "status": "success",
770
+ "message": f"退款成功,{refund_amount}积分已退还",
771
+ "refund_amount": refund_amount,
772
+ "ban_days": REFUND_BAN_DAYS
773
+ }
774
+ except HTTPException:
775
+ db.rollback()
776
+ raise
777
+ except Exception as e:
778
+ db.rollback()
779
+ logger.error(f"REFUND_ERROR | buyer={account} | item={item_id} | amount={refund_amount} | error={str(e)}")
780
  raise HTTPException(status_code=500, detail="退款处理失败,请稍后重试")