ZHIWEI666 commited on
Commit
263221b
·
verified ·
1 Parent(s): 52cacb2

提示词改价24小时延迟,消除并发重复购买竞态

Browse files
Files changed (2) hide show
  1. router_prompts.py +71 -1
  2. router_wallet.py +16 -0
router_prompts.py CHANGED
@@ -111,6 +111,21 @@ def _load_categories() -> dict:
111
  return categories
112
 
113
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
  def _user_owns_prompt(db_session: Session, account: str, prompt_id: str) -> bool:
115
  """检查用户是否已购买(未退款)该提示词"""
116
  if not account:
@@ -263,7 +278,13 @@ async def get_prompts(prompt_type: str = None, category: str = None,
263
 
264
  # 附加作者信息,并过滤敏感字段
265
  result = []
 
266
  for prompt in paged_prompts:
 
 
 
 
 
267
  author_info = user_map.get(prompt.get("author"), {})
268
  prompt_data = {
269
  **prompt,
@@ -275,6 +296,9 @@ async def get_prompts(prompt_type: str = None, category: str = None,
275
  prompt_data.pop("viewed_by", None)
276
  result.append(prompt_data)
277
 
 
 
 
278
  return {
279
  "status": "success",
280
  "data": result,
@@ -296,11 +320,20 @@ async def get_my_prompts(current_user: str = Depends(require_auth)):
296
  my_prompts = sorted(my_prompts, key=lambda x: x.get("created_at", 0), reverse=True)
297
 
298
  result = []
 
299
  for prompt in my_prompts:
 
 
 
 
 
300
  prompt_data = dict(prompt)
301
  prompt_data.pop("viewed_by", None)
302
  result.append(prompt_data)
303
 
 
 
 
304
  return {"status": "success", "data": result}
305
 
306
 
@@ -317,6 +350,12 @@ async def get_prompt_detail(prompt_id: str, current_user: str = Depends(optional
317
 
318
  for prompt in prompts_db:
319
  if prompt["id"] == prompt_id:
 
 
 
 
 
 
320
  author_info = user_map.get(prompt.get("author"), {})
321
  prompt_data = {
322
  **prompt,
@@ -461,7 +500,17 @@ async def update_prompt(prompt_id: str, update_data: PromptUpdate, current_user:
461
  if price < 0 or price > MAX_PRICE:
462
  result_holder["error"] = "invalid_price"
463
  return False
464
- prompt["price"] = price
 
 
 
 
 
 
 
 
 
 
465
  if update_data.media_type is not None:
466
  if update_data.media_type not in ("image", "video"):
467
  result_holder["error"] = "invalid_media_type"
@@ -813,6 +862,12 @@ async def purchase_prompt(request: Request, prompt_id: str,
813
  if not prompt:
814
  raise HTTPException(status_code=404, detail="提示词不存在")
815
 
 
 
 
 
 
 
816
  price = int(prompt.get("price", 0) or 0)
817
  seller_account = prompt.get("author")
818
 
@@ -853,6 +908,21 @@ async def purchase_prompt(request: Request, prompt_id: str,
853
  # 🔒 P1并发安全:使用悲观锁+异常处理+事务回滚防止双花问题
854
  try:
855
  buyer_wallet = db_session.query(Wallet).filter(Wallet.account == current_user).with_for_update().first()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
856
  if not buyer_wallet or buyer_wallet.balance < price:
857
  raise HTTPException(status_code=402, detail="余额不足,请先充值")
858
 
 
111
  return categories
112
 
113
 
114
+ def _apply_effective_price(prompt: dict) -> None:
115
+ """检查 pending_price 是否已过生效时间,若是则更新 price 字段供前端显示(与 router_items.py 同构)"""
116
+ pending_price = prompt.get("pending_price")
117
+ pending_effective = prompt.get("pending_price_effective_at")
118
+ if pending_price is not None and pending_effective:
119
+ try:
120
+ effective_time = datetime.datetime.fromisoformat(pending_effective)
121
+ if datetime.datetime.now() >= effective_time:
122
+ prompt["price"] = pending_price
123
+ prompt["pending_price"] = None
124
+ prompt["pending_price_effective_at"] = None
125
+ except (ValueError, TypeError):
126
+ pass
127
+
128
+
129
  def _user_owns_prompt(db_session: Session, account: str, prompt_id: str) -> bool:
130
  """检查用户是否已购买(未退款)该提示词"""
131
  if not account:
 
278
 
279
  # 附加作者信息,并过滤敏感字段
280
  result = []
281
+ price_updated = False
282
  for prompt in paged_prompts:
283
+ # 💰 检查延迟价格是否已生效(与资源列表同构)
284
+ old_price = prompt.get("price", 0)
285
+ _apply_effective_price(prompt)
286
+ if prompt.get("price", 0) != old_price:
287
+ price_updated = True
288
  author_info = user_map.get(prompt.get("author"), {})
289
  prompt_data = {
290
  **prompt,
 
296
  prompt_data.pop("viewed_by", None)
297
  result.append(prompt_data)
298
 
299
+ if price_updated:
300
+ db.save_data("prompts.json", prompts_db)
301
+
302
  return {
303
  "status": "success",
304
  "data": result,
 
320
  my_prompts = sorted(my_prompts, key=lambda x: x.get("created_at", 0), reverse=True)
321
 
322
  result = []
323
+ price_updated = False
324
  for prompt in my_prompts:
325
+ # 💰 检查延迟价格是否已生效
326
+ old_price = prompt.get("price", 0)
327
+ _apply_effective_price(prompt)
328
+ if prompt.get("price", 0) != old_price:
329
+ price_updated = True
330
  prompt_data = dict(prompt)
331
  prompt_data.pop("viewed_by", None)
332
  result.append(prompt_data)
333
 
334
+ if price_updated:
335
+ db.save_data("prompts.json", prompts_db)
336
+
337
  return {"status": "success", "data": result}
338
 
339
 
 
350
 
351
  for prompt in prompts_db:
352
  if prompt["id"] == prompt_id:
353
+ # 💰 检查延迟价格是否已生效
354
+ old_price = prompt.get("price", 0)
355
+ _apply_effective_price(prompt)
356
+ if prompt.get("price", 0) != old_price:
357
+ db.save_data("prompts.json", prompts_db)
358
+
359
  author_info = user_map.get(prompt.get("author"), {})
360
  prompt_data = {
361
  **prompt,
 
500
  if price < 0 or price > MAX_PRICE:
501
  result_holder["error"] = "invalid_price"
502
  return False
503
+ current_price = int(prompt.get("price", 0) or 0)
504
+ if current_price != price:
505
+ # 设置待生效价格,24小时后生效(与资源改价机制同构;提示词不支持退款,无需禁购配套)
506
+ effective_time = datetime.datetime.now() + datetime.timedelta(hours=24)
507
+ prompt["pending_price"] = price
508
+ prompt["pending_price_effective_at"] = effective_time.isoformat()
509
+ # 不立即修改 price,等待24小时后生效
510
+ else:
511
+ # 价格未变,清除待生效价格
512
+ prompt["pending_price"] = None
513
+ prompt["pending_price_effective_at"] = None
514
  if update_data.media_type is not None:
515
  if update_data.media_type not in ("image", "video"):
516
  result_holder["error"] = "invalid_media_type"
 
862
  if not prompt:
863
  raise HTTPException(status_code=404, detail="提示词不存在")
864
 
865
+ # 💰 应用延迟生效价格(与 /api/wallet/purchase 同构),确保扣费价与前端显示价一致
866
+ old_price = prompt.get("price", 0)
867
+ _apply_effective_price(prompt)
868
+ if prompt.get("price", 0) != old_price:
869
+ db.save_data("prompts.json", prompts_db)
870
+
871
  price = int(prompt.get("price", 0) or 0)
872
  seller_account = prompt.get("author")
873
 
 
908
  # 🔒 P1并发安全:使用悲观锁+异常处理+事务回滚防止双花问题
909
  try:
910
  buyer_wallet = db_session.query(Wallet).filter(Wallet.account == current_user).with_for_update().first()
911
+
912
+ # 🔒 锁内二次检查:串行化后重查 Ownership,彻底消除并发重复购买竞态
913
+ owned_locked = db_session.query(Ownership).filter(
914
+ Ownership.account == current_user,
915
+ Ownership.item_id == prompt_id,
916
+ Ownership.is_refunded == False
917
+ ).first()
918
+ if owned_locked:
919
+ db_session.rollback()
920
+ return {
921
+ "status": "success",
922
+ "already_owned": True,
923
+ "prompt_text": prompt.get("prompt_text", "")
924
+ }
925
+
926
  if not buyer_wallet or buyer_wallet.balance < price:
927
  raise HTTPException(status_code=402, detail="余额不足,请先充值")
928
 
router_wallet.py CHANGED
@@ -402,6 +402,22 @@ async def purchase_item(request: Request, req: PurchaseRequest, current_user: st
402
  # 🔒 P1并发安全:使用悲观锁+异常处理+事务回滚防止双花问题
403
  try:
404
  buyer_wallet = db.query(Wallet).filter(Wallet.account == req.account).with_for_update().first()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
405
  if not buyer_wallet or buyer_wallet.balance < price:
406
  raise HTTPException(status_code=402, detail="余额不足,请先充值")
407
 
 
402
  # 🔒 P1并发安全:使用悲观锁+异常处理+事务回滚防止双花问题
403
  try:
404
  buyer_wallet = db.query(Wallet).filter(Wallet.account == req.account).with_for_update().first()
405
+
406
+ # 🔒 锁内二次检查:串行化后重查 Ownership,彻底消除并发重复购买竞态
407
+ owned_locked = db.query(Ownership).filter(
408
+ Ownership.account == req.account,
409
+ Ownership.item_id == req.item_id,
410
+ Ownership.is_refunded == False
411
+ ).first()
412
+ if owned_locked:
413
+ db.rollback()
414
+ return {
415
+ "status": "success",
416
+ "already_owned": True,
417
+ "netdisk_password": item.get("netdisk_password"), # ☁️
418
+ "is_netdisk": item.get("is_netdisk", False) # ☁️
419
+ }
420
+
421
  if not buyer_wallet or buyer_wallet.balance < price:
422
  raise HTTPException(status_code=402, detail="余额不足,请先充值")
423