Spaces:
Running
Running
提示词改价24小时延迟,消除并发重复购买竞态
Browse files- router_prompts.py +71 -1
- 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
|