Spaces:
Running
Running
Upload 14 files
Browse files- requirements.txt +1 -2
- router_items.py +161 -99
- router_wallet.py +223 -134
requirements.txt
CHANGED
|
@@ -9,5 +9,4 @@ sqlalchemy
|
|
| 9 |
psycopg2-binary
|
| 10 |
httpx
|
| 11 |
python-alipay-sdk
|
| 12 |
-
aiofiles
|
| 13 |
-
bleach
|
|
|
|
| 9 |
psycopg2-binary
|
| 10 |
httpx
|
| 11 |
python-alipay-sdk
|
| 12 |
+
aiofiles
|
|
|
router_items.py
CHANGED
|
@@ -1,115 +1,177 @@
|
|
| 1 |
-
|
| 2 |
-
import json
|
| 3 |
-
import time
|
| 4 |
from fastapi import APIRouter, HTTPException
|
| 5 |
-
|
| 6 |
-
|
|
|
|
|
|
|
|
|
|
| 7 |
|
| 8 |
router = APIRouter()
|
| 9 |
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
createdAt: str
|
| 22 |
-
updatedAt: str
|
| 23 |
-
isPurchased: bool = False
|
| 24 |
-
|
| 25 |
-
class ItemUpdate(BaseModel):
|
| 26 |
-
name: str = None
|
| 27 |
-
description: str = None
|
| 28 |
-
fullDesc: str = None
|
| 29 |
-
author: str = None
|
| 30 |
-
tags: list = None
|
| 31 |
-
price: float = None
|
| 32 |
-
thumbnail: str = None
|
| 33 |
-
repoUrl: str = None
|
| 34 |
-
|
| 35 |
-
class PurchaseRequest(BaseModel):
|
| 36 |
-
account: str
|
| 37 |
-
item_id: str
|
| 38 |
-
|
| 39 |
-
# 加载数据
|
| 40 |
-
def load_data():
|
| 41 |
-
with open("items.json", "r") as f:
|
| 42 |
-
return json.load(f)
|
| 43 |
-
|
| 44 |
-
# 保存数据
|
| 45 |
-
def save_data(data):
|
| 46 |
-
with open("items.json", "w") as f:
|
| 47 |
-
json.dump(data, f, indent=2)
|
| 48 |
-
|
| 49 |
-
@router.get("/items")
|
| 50 |
-
async def get_items():
|
| 51 |
-
return load_data()
|
| 52 |
-
|
| 53 |
-
@router.get("/item/{item_id}")
|
| 54 |
-
async def get_item(item_id: str):
|
| 55 |
-
items = load_data()
|
| 56 |
-
item = next((i for i in items if i["id"] == item_id), None)
|
| 57 |
-
if not item:
|
| 58 |
-
raise HTTPException(status_code=404, detail="Item not found")
|
| 59 |
-
return item
|
| 60 |
|
| 61 |
-
@router.
|
| 62 |
-
async def
|
| 63 |
-
|
|
|
|
| 64 |
|
| 65 |
-
#
|
| 66 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
|
| 68 |
-
|
| 69 |
-
if any(i["id"] == item.id for i in items):
|
| 70 |
-
raise HTTPException(status_code=400, detail="Item already exists")
|
| 71 |
-
|
| 72 |
-
items.append(item.dict())
|
| 73 |
-
save_data(items)
|
| 74 |
-
return {"status": "success", "message": "Item created"}
|
| 75 |
|
| 76 |
-
@router.
|
| 77 |
-
async def
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
if not target_item:
|
| 82 |
-
raise HTTPException(status_code=404, detail="Item not found")
|
| 83 |
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
item.fullDesc = clean_html(item.fullDesc)
|
| 87 |
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
|
| 94 |
-
|
| 95 |
-
return {"status": "success", "message": "Item updated"}
|
| 96 |
|
| 97 |
-
@router.post("/
|
| 98 |
-
async def
|
| 99 |
-
|
| 100 |
-
item =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
|
| 102 |
-
if
|
| 103 |
-
|
| 104 |
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
raise HTTPException(status_code=400, detail="Item already purchased")
|
| 108 |
|
| 109 |
-
|
| 110 |
-
if
|
| 111 |
-
|
| 112 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
|
| 114 |
-
|
| 115 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# router_items.py
|
|
|
|
|
|
|
| 2 |
from fastapi import APIRouter, HTTPException
|
| 3 |
+
import time
|
| 4 |
+
import uuid
|
| 5 |
+
import datetime
|
| 6 |
+
import 数据库连接 as db
|
| 7 |
+
from models import ItemCreate, ItemUpdate
|
| 8 |
|
| 9 |
router = APIRouter()
|
| 10 |
|
| 11 |
+
def get_last_6_months():
|
| 12 |
+
res = []
|
| 13 |
+
today = datetime.date.today()
|
| 14 |
+
for i in range(5, -1, -1):
|
| 15 |
+
m = today.month - i
|
| 16 |
+
y = today.year
|
| 17 |
+
while m <= 0:
|
| 18 |
+
m += 12
|
| 19 |
+
y -= 1
|
| 20 |
+
res.append(f"{y}-{m:02d}")
|
| 21 |
+
return res
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
|
| 23 |
+
@router.get("/api/items")
|
| 24 |
+
async def get_items(type: str = "tool", sort: str = "time", limit: int = 50): # 优化:默认限制调大至 50,提升前端列表体验
|
| 25 |
+
items_db = db.load_data("items.json", default_data=[])
|
| 26 |
+
comments_db = db.load_data("comments.json", default_data={})
|
| 27 |
|
| 28 |
+
# 如果是推荐榜,匹配所有 recommend 开头的子类型
|
| 29 |
+
if type == "recommend":
|
| 30 |
+
filtered_items = [item for item in items_db if item.get("type", "").startswith("recommend")]
|
| 31 |
+
else:
|
| 32 |
+
filtered_items = [item for item in items_db if item.get("type") == type]
|
| 33 |
+
|
| 34 |
+
for item in filtered_items:
|
| 35 |
+
item["commentsData"] = comments_db.get(item["id"], [])
|
| 36 |
+
item["comments"] = len(item["commentsData"])
|
| 37 |
+
|
| 38 |
+
# 🔴 【绝对核心防线】:在下发给前端前,强行在内存中抹除创作者的 Token!
|
| 39 |
+
# 这样即使资源是公开展示的,普通用户也绝对抓不到源仓库的密钥。
|
| 40 |
+
item.pop("github_token", None)
|
| 41 |
+
|
| 42 |
+
if sort == "likes": filtered_items.sort(key=lambda x: x.get("likes", 0), reverse=True)
|
| 43 |
+
elif sort == "favorites": filtered_items.sort(key=lambda x: x.get("favorites", 0), reverse=True)
|
| 44 |
+
elif sort == "downloads": filtered_items.sort(key=lambda x: x.get("uses", 0), reverse=True)
|
| 45 |
+
else: filtered_items.sort(key=lambda x: x.get("created_at", 0), reverse=True)
|
| 46 |
|
| 47 |
+
return {"status": "success", "data": filtered_items[:limit]}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
|
| 49 |
+
@router.get("/api/creators")
|
| 50 |
+
async def get_creators(sort: str = "downloads", limit: int = 20):
|
| 51 |
+
users_db = db.load_data("users.json", default_data={})
|
| 52 |
+
items_db = db.load_data("items.json", default_data=[])
|
| 53 |
+
comments_db = db.load_data("comments.json", default_data={})
|
|
|
|
|
|
|
| 54 |
|
| 55 |
+
creators = []
|
| 56 |
+
months = get_last_6_months()
|
|
|
|
| 57 |
|
| 58 |
+
for account, u in users_db.items():
|
| 59 |
+
u_items = [i for i in items_db if i.get("author") == account]
|
| 60 |
+
|
| 61 |
+
trend_tools = {m: 0 for m in months}
|
| 62 |
+
trend_apps = {m: 0 for m in months}
|
| 63 |
+
trend_recommends = {m: 0 for m in months}
|
| 64 |
+
tools_count = 0
|
| 65 |
+
apps_count = 0
|
| 66 |
+
|
| 67 |
+
for i in u_items:
|
| 68 |
+
itype = i.get("type", "")
|
| 69 |
+
history = i.get("use_history", {})
|
| 70 |
+
if itype == "tool" or itype == "recommend_tool":
|
| 71 |
+
if itype == "tool": tools_count += 1
|
| 72 |
+
for m in months: trend_tools[m] += history.get(m, 0)
|
| 73 |
+
elif itype == "app" or itype == "recommend_app":
|
| 74 |
+
if itype == "app": apps_count += 1
|
| 75 |
+
for m in months: trend_apps[m] += history.get(m, 0)
|
| 76 |
+
elif itype.startswith("recommend"):
|
| 77 |
+
for m in months: trend_recommends[m] += history.get(m, 0)
|
| 78 |
+
|
| 79 |
+
creators.append({
|
| 80 |
+
"account": account, "name": u.get("name", account), "avatar": u.get("avatarDataUrl", "https://via.placeholder.com/150"),
|
| 81 |
+
"shortDesc": u.get("intro", "这个人很懒,什么都没写..."), "fullDesc": u.get("intro", "这个人很懒,什么都没写..."),
|
| 82 |
+
"likes": sum(i.get("likes", 0) for i in u_items), "favorites": sum(i.get("favorites", 0) for i in u_items),
|
| 83 |
+
"downloads": sum(i.get("uses", 0) for i in u_items),
|
| 84 |
+
"toolsCount": tools_count, "appsCount": apps_count, "followers": len(u.get("followers", [])), "created_at": u.get("created_at", 0),
|
| 85 |
+
"commentsData": comments_db.get(account, []),
|
| 86 |
+
"trendData": {
|
| 87 |
+
"months": months,
|
| 88 |
+
"tools": [trend_tools[m] for m in months],
|
| 89 |
+
"apps": [trend_apps[m] for m in months],
|
| 90 |
+
"recommends": [trend_recommends[m] for m in months]
|
| 91 |
+
}
|
| 92 |
+
})
|
| 93 |
+
|
| 94 |
+
if sort == "likes": creators.sort(key=lambda x: x.get("likes", 0), reverse=True)
|
| 95 |
+
elif sort == "favorites": creators.sort(key=lambda x: x.get("favorites", 0), reverse=True)
|
| 96 |
+
elif sort == "downloads": creators.sort(key=lambda x: x.get("downloads", 0), reverse=True)
|
| 97 |
+
else: creators.sort(key=lambda x: x.get("created_at", 0), reverse=True)
|
| 98 |
|
| 99 |
+
return {"status": "success", "data": creators[:limit]}
|
|
|
|
| 100 |
|
| 101 |
+
@router.post("/api/items")
|
| 102 |
+
async def create_item(item: ItemCreate):
|
| 103 |
+
# 【安全加固】:强制转换为整数,并拦截负数 (防浮点漏洞与洗钱)
|
| 104 |
+
item.price = int(item.price)
|
| 105 |
+
if item.price < 0:
|
| 106 |
+
raise HTTPException(status_code=400, detail="🚨 安全拦截:商品价格不能为负数")
|
| 107 |
+
|
| 108 |
+
items_db = db.load_data("items.json", default_data=[])
|
| 109 |
+
new_item = {
|
| 110 |
+
"id": f"{item.type}_{int(time.time())}_{uuid.uuid4().hex[:6]}", "type": item.type, "title": item.title, "author": item.author,
|
| 111 |
+
"shortDesc": item.shortDesc, "fullDesc": item.fullDesc, "link": item.link, "coverUrl": item.coverUrl, "price": item.price,
|
| 112 |
+
"github_token": item.github_token, # 【新增】保存密钥到云端 JSON
|
| 113 |
+
"likes": 0, "favorites": 0, "comments": 0, "uses": 0, "use_history": {}, "created_at": int(time.time()), "liked_by": [], "favorited_by": []
|
| 114 |
+
}
|
| 115 |
+
items_db.insert(0, new_item)
|
| 116 |
+
db.save_data("items.json", items_db)
|
| 117 |
+
return {"status": "success", "data": new_item}
|
| 118 |
+
|
| 119 |
+
@router.put("/api/items/{item_id}")
|
| 120 |
+
async def update_item(item_id: str, update_data: ItemUpdate, author: str):
|
| 121 |
+
# 【安全加固】:更新时同样强制转换为整数并拦截负数
|
| 122 |
+
if update_data.price is not None:
|
| 123 |
+
update_data.price = int(update_data.price)
|
| 124 |
+
if update_data.price < 0:
|
| 125 |
+
raise HTTPException(status_code=400, detail="🚨 安全拦截:商品价格不能为负数")
|
| 126 |
+
|
| 127 |
+
items_db = db.load_data("items.json", default_data=[])
|
| 128 |
+
for item in items_db:
|
| 129 |
+
if item["id"] == item_id:
|
| 130 |
+
if item.get("author") != author: raise HTTPException(status_code=403, detail="无权修改他人发布的内容")
|
| 131 |
+
|
| 132 |
+
if update_data.title is not None: item["title"] = update_data.title
|
| 133 |
+
if update_data.shortDesc is not None: item["shortDesc"] = update_data.shortDesc
|
| 134 |
+
if update_data.fullDesc is not None: item["fullDesc"] = update_data.fullDesc
|
| 135 |
+
if update_data.link is not None: item["link"] = update_data.link
|
| 136 |
+
if update_data.coverUrl is not None: item["coverUrl"] = update_data.coverUrl
|
| 137 |
+
if update_data.price is not None: item["price"] = update_data.price
|
| 138 |
+
if update_data.github_token is not None: item["github_token"] = update_data.github_token # 【新增】允许更新密钥
|
| 139 |
+
|
| 140 |
+
db.save_data("items.json", items_db)
|
| 141 |
+
return {"status": "success"}
|
| 142 |
+
|
| 143 |
+
raise HTTPException(status_code=404, detail="找不到该内容记录")
|
| 144 |
+
|
| 145 |
+
@router.delete("/api/items/{item_id}")
|
| 146 |
+
async def delete_item(item_id: str, author: str):
|
| 147 |
+
items_db = db.load_data("items.json", default_data=[])
|
| 148 |
+
target_idx = next((i for i, item in enumerate(items_db) if item["id"] == item_id), None)
|
| 149 |
|
| 150 |
+
if target_idx is None: raise HTTPException(status_code=404, detail="找不到该内容记录")
|
| 151 |
+
if items_db[target_idx].get("author") != author: raise HTTPException(status_code=403, detail="无权删除他人发布的内容")
|
| 152 |
|
| 153 |
+
items_db.pop(target_idx)
|
| 154 |
+
db.save_data("items.json", items_db)
|
|
|
|
| 155 |
|
| 156 |
+
comments_db = db.load_data("comments.json", default_data={})
|
| 157 |
+
if item_id in comments_db:
|
| 158 |
+
del comments_db[item_id]
|
| 159 |
+
db.save_data("comments.json", comments_db)
|
| 160 |
+
|
| 161 |
+
return {"status": "success"}
|
| 162 |
+
|
| 163 |
+
@router.post("/api/items/{item_id}/use")
|
| 164 |
+
async def record_item_use(item_id: str):
|
| 165 |
+
items_db = db.load_data("items.json", default_data=[])
|
| 166 |
+
current_month = datetime.date.today().strftime("%Y-%m")
|
| 167 |
|
| 168 |
+
for item in items_db:
|
| 169 |
+
if item["id"] == item_id:
|
| 170 |
+
item["uses"] = item.get("uses", 0) + 1
|
| 171 |
+
if "use_history" not in item:
|
| 172 |
+
item["use_history"] = {}
|
| 173 |
+
item["use_history"][current_month] = item["use_history"].get(current_month, 0) + 1
|
| 174 |
+
db.save_data("items.json", items_db)
|
| 175 |
+
return {"status": "success", "uses": item["uses"]}
|
| 176 |
+
|
| 177 |
+
raise HTTPException(status_code=404, detail="找不到该内容记录")
|
router_wallet.py
CHANGED
|
@@ -1,149 +1,238 @@
|
|
| 1 |
-
|
| 2 |
-
import
|
|
|
|
|
|
|
| 3 |
import time
|
| 4 |
-
import
|
| 5 |
import hashlib
|
| 6 |
-
|
| 7 |
-
from
|
| 8 |
-
from
|
| 9 |
-
from
|
| 10 |
-
|
| 11 |
-
from functools import lru_cache # 新增导入
|
| 12 |
|
| 13 |
router = APIRouter()
|
| 14 |
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
transaction_type: str # 'deposit' or 'withdraw'
|
| 29 |
-
description: str
|
| 30 |
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
|
| 49 |
-
@
|
| 50 |
-
def
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
|
|
|
| 58 |
|
| 59 |
-
@router.post("/
|
| 60 |
-
async def
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
if not
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
# 检查交易状态
|
| 68 |
-
trade_status = params.get("trade_status")
|
| 69 |
-
if trade_status != "TRADE_SUCCESS":
|
| 70 |
-
return JSONResponse(content={"status": "error", "message": "Invalid trade status"}, status_code=400)
|
| 71 |
-
|
| 72 |
-
# 获取交易号
|
| 73 |
-
trade_no = params.get("out_trade_no")
|
| 74 |
-
|
| 75 |
-
# 检查是否已处理(使用缓存)
|
| 76 |
-
if check_idempotency(trade_no):
|
| 77 |
-
return JSONResponse(content={"status": "success", "message": "重复通知已忽略"})
|
| 78 |
-
|
| 79 |
-
# 插入幂等记录
|
| 80 |
-
with SessionLocal() as session:
|
| 81 |
-
session.execute(
|
| 82 |
-
text("INSERT INTO idempotency (trade_no) VALUES (:trade_no)"),
|
| 83 |
-
{"trade_no": trade_no}
|
| 84 |
-
)
|
| 85 |
-
session.commit()
|
| 86 |
-
|
| 87 |
-
# 处理支付成功逻辑
|
| 88 |
-
account = params.get("buyer_id")
|
| 89 |
-
amount = float(params.get("total_amount"))
|
| 90 |
-
|
| 91 |
-
# 更新用户余额
|
| 92 |
-
with SessionLocal() as session:
|
| 93 |
-
# 假设有一个users表存储用户信息
|
| 94 |
-
user = session.execute(
|
| 95 |
-
text("SELECT * FROM users WHERE account = :account"),
|
| 96 |
-
{"account": account}
|
| 97 |
-
).fetchone()
|
| 98 |
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
|
| 125 |
-
@router.post("/
|
| 126 |
-
async def
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
)
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 144 |
|
| 145 |
-
@router.
|
| 146 |
-
async def
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# router_wallet.py
|
| 2 |
+
from fastapi import APIRouter, Depends, HTTPException, Request
|
| 3 |
+
from fastapi.responses import Response
|
| 4 |
+
from sqlalchemy.orm import Session
|
| 5 |
import time
|
| 6 |
+
import uuid
|
| 7 |
import hashlib
|
| 8 |
+
import os
|
| 9 |
+
from database_sql import get_db
|
| 10 |
+
from models_sql import Wallet, Transaction, Ownership
|
| 11 |
+
from models import RechargeRequest, WithdrawRequest, PurchaseRequest, TipRequest
|
| 12 |
+
import 数据库连接 as json_db
|
|
|
|
| 13 |
|
| 14 |
router = APIRouter()
|
| 15 |
|
| 16 |
+
try:
|
| 17 |
+
from alipay import AliPay
|
| 18 |
+
from alipay.utils import AliPayConfig
|
| 19 |
+
alipay = AliPay(
|
| 20 |
+
appid=os.environ.get("ALIPAY_APPID", ""),
|
| 21 |
+
app_notify_url="https://zhiwei666-comfyui-ranking-api.hf.space/api/wallet/alipay_notify",
|
| 22 |
+
app_private_key_string=os.environ.get("ALIPAY_PRIVATE_KEY", "").replace("\\n", "\n"),
|
| 23 |
+
alipay_public_key_string=os.environ.get("ALIPAY_PUBLIC_KEY", "").replace("\\n", "\n"),
|
| 24 |
+
sign_type="RSA2",
|
| 25 |
+
debug=False,
|
| 26 |
+
config=AliPayConfig(timeout=15)
|
| 27 |
+
)
|
| 28 |
+
except Exception as e:
|
| 29 |
+
alipay = None
|
| 30 |
|
| 31 |
+
def calculate_tx_hash(tx_id, account, tx_type, amount, prev_hash):
|
| 32 |
+
data = f"{tx_id}{account}{tx_type}{amount}{prev_hash}"
|
| 33 |
+
return hashlib.sha256(data.encode()).hexdigest()
|
|
|
|
|
|
|
| 34 |
|
| 35 |
+
@router.post("/api/wallet/create_recharge_order")
|
| 36 |
+
async def create_recharge_order(req: RechargeRequest):
|
| 37 |
+
if not alipay:
|
| 38 |
+
raise HTTPException(status_code=500, detail="支付网关未配置或初始化失败")
|
| 39 |
+
|
| 40 |
+
order_id = f"PAY_{int(time.time())}_{uuid.uuid4().hex[:6]}"
|
| 41 |
+
subject = f"ComfyUI Community Points - {req.account}"
|
| 42 |
+
|
| 43 |
+
order_string = alipay.api_alipay_trade_precreate(
|
| 44 |
+
out_trade_no=order_id,
|
| 45 |
+
total_amount=str(req.amount),
|
| 46 |
+
subject=subject
|
| 47 |
+
)
|
| 48 |
+
|
| 49 |
+
qr_code_url = order_string.get("qr_code")
|
| 50 |
+
if not qr_code_url:
|
| 51 |
+
raise HTTPException(status_code=500, detail="生成支付二维码失败")
|
| 52 |
+
|
| 53 |
+
return {"status": "success", "order_id": order_id, "qr_code": qr_code_url}
|
| 54 |
|
| 55 |
+
# 🟢 业务流转细节修复:正确解析 application/x-www-form-urlencoded
|
| 56 |
+
@router.post("/api/wallet/alipay_notify")
|
| 57 |
+
async def alipay_notify(request: Request, db: Session = Depends(get_db)):
|
| 58 |
+
# 强制将表单数据解析为纯字典,防止由于数据类型错误导致验签失败
|
| 59 |
+
form_data = await request.form()
|
| 60 |
+
data = dict(form_data.items())
|
| 61 |
+
|
| 62 |
+
signature = data.pop("sign", None)
|
| 63 |
+
data.pop("sign_type", None)
|
| 64 |
+
|
| 65 |
+
if not alipay or not signature or not alipay.verify(data, signature):
|
| 66 |
+
return Response(content="fail", media_type="text/plain")
|
| 67 |
+
|
| 68 |
+
if data.get("trade_status") in ("TRADE_SUCCESS", "TRADE_FINISHED"):
|
| 69 |
+
order_id = data.get("out_trade_no")
|
| 70 |
|
| 71 |
+
existing_tx = db.query(Transaction).filter(Transaction.tx_id == order_id).first()
|
| 72 |
+
if not existing_tx:
|
| 73 |
+
amount = int(float(data.get("total_amount", 0)))
|
| 74 |
+
account = data.get("subject", "").split(" - ")[-1]
|
| 75 |
+
|
| 76 |
+
wallet = db.query(Wallet).filter(Wallet.account == account).with_for_update().first()
|
| 77 |
+
if not wallet:
|
| 78 |
+
wallet = Wallet(account=account)
|
| 79 |
+
db.add(wallet)
|
| 80 |
+
|
| 81 |
+
wallet.balance += amount
|
| 82 |
+
|
| 83 |
+
last_tx = db.query(Transaction).filter(Transaction.account == account).order_by(Transaction.created_at.desc()).first()
|
| 84 |
+
prev_hash = last_tx.tx_hash if last_tx else "GENESIS_HASH"
|
| 85 |
+
tx_hash = calculate_tx_hash(order_id, account, "RECHARGE", amount, prev_hash)
|
| 86 |
+
|
| 87 |
+
new_tx = Transaction(
|
| 88 |
+
tx_id=order_id, account=account, tx_type="RECHARGE", amount=amount,
|
| 89 |
+
prev_hash=prev_hash, tx_hash=tx_hash
|
| 90 |
+
)
|
| 91 |
+
db.add(new_tx)
|
| 92 |
+
db.commit()
|
| 93 |
+
|
| 94 |
+
return Response(content="success", media_type="text/plain")
|
| 95 |
+
|
| 96 |
+
@router.get("/api/wallet/check_order/{order_id}")
|
| 97 |
+
async def check_order(order_id: str, db: Session = Depends(get_db)):
|
| 98 |
+
tx = db.query(Transaction).filter(Transaction.tx_id == order_id).first()
|
| 99 |
+
if tx:
|
| 100 |
+
return {"status": "SUCCESS"}
|
| 101 |
+
return {"status": "PENDING"}
|
| 102 |
|
| 103 |
+
@router.get("/api/wallet/{account}")
|
| 104 |
+
async def get_wallet(account: str, db: Session = Depends(get_db)):
|
| 105 |
+
wallet = db.query(Wallet).filter(Wallet.account == account).first()
|
| 106 |
+
if not wallet:
|
| 107 |
+
return {"balance": 0, "earn_balance": 0, "tip_balance": 0}
|
| 108 |
+
return {
|
| 109 |
+
"balance": wallet.balance,
|
| 110 |
+
"earn_balance": wallet.earn_balance,
|
| 111 |
+
"tip_balance": wallet.tip_balance
|
| 112 |
+
}
|
| 113 |
|
| 114 |
+
@router.post("/api/wallet/purchase")
|
| 115 |
+
async def purchase_item(req: PurchaseRequest, db: Session = Depends(get_db)):
|
| 116 |
+
items_db = json_db.load_data("items.json", default_data=[])
|
| 117 |
+
item = next((i for i in items_db if i["id"] == req.item_id), None)
|
| 118 |
+
|
| 119 |
+
if not item:
|
| 120 |
+
raise HTTPException(status_code=404, detail="商品不存在")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 121 |
|
| 122 |
+
price = int(item.get("price", 0))
|
| 123 |
+
seller_account = item.get("author")
|
| 124 |
+
|
| 125 |
+
if price <= 0 or req.account == seller_account:
|
| 126 |
+
return {"status": "success", "already_owned": True}
|
| 127 |
+
|
| 128 |
+
owned = db.query(Ownership).filter(Ownership.account == req.account, Ownership.item_id == req.item_id).first()
|
| 129 |
+
if owned:
|
| 130 |
+
return {"status": "success", "already_owned": True}
|
| 131 |
+
|
| 132 |
+
buyer_wallet = db.query(Wallet).filter(Wallet.account == req.account).with_for_update().first()
|
| 133 |
+
if not buyer_wallet or buyer_wallet.balance < price:
|
| 134 |
+
raise HTTPException(status_code=402, detail="余额不足,请先充值")
|
| 135 |
+
|
| 136 |
+
seller_wallet = db.query(Wallet).filter(Wallet.account == seller_account).with_for_update().first()
|
| 137 |
+
if not seller_wallet:
|
| 138 |
+
seller_wallet = Wallet(account=seller_account)
|
| 139 |
+
db.add(seller_wallet)
|
| 140 |
|
| 141 |
+
buyer_wallet.balance -= price
|
| 142 |
+
seller_wallet.earn_balance += price
|
| 143 |
+
|
| 144 |
+
new_ownership = Ownership(account=req.account, item_id=req.item_id)
|
| 145 |
+
db.add(new_ownership)
|
| 146 |
+
|
| 147 |
+
tx_id = f"BUY_{int(time.time())}_{uuid.uuid4().hex[:6]}"
|
| 148 |
+
last_tx = db.query(Transaction).filter(Transaction.account == req.account).order_by(Transaction.created_at.desc()).first()
|
| 149 |
+
prev_hash = last_tx.tx_hash if last_tx else "GENESIS_HASH"
|
| 150 |
+
tx_hash = calculate_tx_hash(tx_id, req.account, "PURCHASE", -price, prev_hash)
|
| 151 |
+
|
| 152 |
+
new_tx = Transaction(
|
| 153 |
+
tx_id=tx_id, account=req.account, tx_type="PURCHASE", amount=-price,
|
| 154 |
+
target_account=seller_account, prev_hash=prev_hash, tx_hash=tx_hash
|
| 155 |
+
)
|
| 156 |
+
db.add(new_tx)
|
| 157 |
+
db.commit()
|
| 158 |
+
|
| 159 |
+
return {"status": "success", "already_owned": False}
|
| 160 |
|
| 161 |
+
@router.post("/api/wallet/tip")
|
| 162 |
+
async def tip_user(req: TipRequest, db: Session = Depends(get_db)):
|
| 163 |
+
if req.amount <= 0:
|
| 164 |
+
raise HTTPException(status_code=400, detail="打赏金额必须大于0")
|
| 165 |
+
|
| 166 |
+
sender_wallet = db.query(Wallet).filter(Wallet.account == req.sender_account).with_for_update().first()
|
| 167 |
+
if not sender_wallet or sender_wallet.balance < req.amount:
|
| 168 |
+
raise HTTPException(status_code=402, detail="余额不足")
|
| 169 |
+
|
| 170 |
+
target_wallet = db.query(Wallet).filter(Wallet.account == req.target_account).with_for_update().first()
|
| 171 |
+
if not target_wallet:
|
| 172 |
+
target_wallet = Wallet(account=req.target_account)
|
| 173 |
+
db.add(target_wallet)
|
| 174 |
+
|
| 175 |
+
sender_wallet.balance -= req.amount
|
| 176 |
+
target_wallet.tip_balance += req.amount
|
| 177 |
+
|
| 178 |
+
tx_id = f"TIP_{int(time.time())}_{uuid.uuid4().hex[:6]}"
|
| 179 |
+
last_tx = db.query(Transaction).filter(Transaction.account == req.sender_account).order_by(Transaction.created_at.desc()).first()
|
| 180 |
+
prev_hash = last_tx.tx_hash if last_tx else "GENESIS_HASH"
|
| 181 |
+
tx_hash = calculate_tx_hash(tx_id, req.sender_account, "TIP", -req.amount, prev_hash)
|
| 182 |
+
|
| 183 |
+
new_tx = Transaction(
|
| 184 |
+
tx_id=tx_id, account=req.sender_account, tx_type="TIP", amount=-req.amount,
|
| 185 |
+
target_account=req.target_account, prev_hash=prev_hash, tx_hash=tx_hash
|
| 186 |
+
)
|
| 187 |
+
db.add(new_tx)
|
| 188 |
+
db.commit()
|
| 189 |
+
|
| 190 |
+
from notifications import add_notification
|
| 191 |
+
display_sender = "匿名用户" if req.is_anonymous else req.sender_account
|
| 192 |
+
add_notification(req.target_account, {
|
| 193 |
+
"type": "tip",
|
| 194 |
+
"from_user": "system",
|
| 195 |
+
"target_item_title": "您的主页",
|
| 196 |
+
"content": f"🎉 {display_sender} 给您打赏了 {req.amount} 积分!"
|
| 197 |
+
})
|
| 198 |
+
|
| 199 |
+
return {"status": "success", "balance": sender_wallet.balance}
|
| 200 |
|
| 201 |
+
@router.post("/api/wallet/withdraw")
|
| 202 |
+
async def withdraw(req: WithdrawRequest, db: Session = Depends(get_db)):
|
| 203 |
+
key = f"{req.account}_withdraw"
|
| 204 |
+
code_data = VERIFY_CODES.get(key)
|
| 205 |
+
if not code_data or code_data["code"] != req.code or time.time() > code_data["expires"]:
|
| 206 |
+
raise HTTPException(status_code=400, detail="验证码无效或已过期")
|
| 207 |
+
|
| 208 |
+
wallet = db.query(Wallet).filter(Wallet.account == req.account).with_for_update().first()
|
| 209 |
+
if not wallet:
|
| 210 |
+
raise HTTPException(status_code=400, detail="钱包不存在")
|
| 211 |
+
|
| 212 |
+
total_withdrawable = wallet.earn_balance + wallet.tip_balance
|
| 213 |
+
if req.amount > total_withdrawable:
|
| 214 |
+
raise HTTPException(status_code=400, detail="可提现余额不足")
|
| 215 |
+
|
| 216 |
+
if req.amount <= wallet.earn_balance:
|
| 217 |
+
wallet.earn_balance -= req.amount
|
| 218 |
+
else:
|
| 219 |
+
remaining = req.amount - wallet.earn_balance
|
| 220 |
+
wallet.earn_balance = 0
|
| 221 |
+
wallet.tip_balance -= remaining
|
| 222 |
+
|
| 223 |
+
wallet.frozen_balance += req.amount
|
| 224 |
+
|
| 225 |
+
tx_id = f"WD_{int(time.time())}_{uuid.uuid4().hex[:6]}"
|
| 226 |
+
last_tx = db.query(Transaction).filter(Transaction.account == req.account).order_by(Transaction.created_at.desc()).first()
|
| 227 |
+
prev_hash = last_tx.tx_hash if last_tx else "GENESIS_HASH"
|
| 228 |
+
tx_hash = calculate_tx_hash(tx_id, req.account, "WITHDRAW", -req.amount, prev_hash)
|
| 229 |
+
|
| 230 |
+
new_tx = Transaction(
|
| 231 |
+
tx_id=tx_id, account=req.account, tx_type="WITHDRAW", amount=-req.amount,
|
| 232 |
+
prev_hash=prev_hash, tx_hash=tx_hash
|
| 233 |
+
)
|
| 234 |
+
db.add(new_tx)
|
| 235 |
+
db.commit()
|
| 236 |
+
|
| 237 |
+
del VERIFY_CODES[key]
|
| 238 |
+
return {"status": "success"}
|