Spaces:
Running
Running
Upload 17 files
Browse files- idempotency_table.sql +4 -0
- init_db.py +42 -0
- models.py +3 -5
- nginx.conf +54 -0
- router_items.py +99 -161
- router_wallet.py +134 -223
- xss_filter.py +25 -0
idempotency_table.sql
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
CREATE TABLE idempotency (
|
| 2 |
+
trade_no VARCHAR(64) PRIMARY KEY,
|
| 3 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
| 4 |
+
);
|
init_db.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from sqlalchemy import create_engine, text
|
| 3 |
+
|
| 4 |
+
# 数据库配置
|
| 5 |
+
DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///./test.db")
|
| 6 |
+
engine = create_engine(DATABASE_URL)
|
| 7 |
+
|
| 8 |
+
def init_database():
|
| 9 |
+
# 创建 idempotency 表
|
| 10 |
+
with engine.connect() as conn:
|
| 11 |
+
conn.execute(text("""
|
| 12 |
+
CREATE TABLE IF NOT EXISTS idempotency (
|
| 13 |
+
trade_no VARCHAR(64) PRIMARY KEY,
|
| 14 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
| 15 |
+
)
|
| 16 |
+
"""))
|
| 17 |
+
|
| 18 |
+
# 创建 users 表
|
| 19 |
+
conn.execute(text("""
|
| 20 |
+
CREATE TABLE IF NOT EXISTS users (
|
| 21 |
+
account VARCHAR(255) PRIMARY KEY,
|
| 22 |
+
balance FLOAT DEFAULT 0
|
| 23 |
+
)
|
| 24 |
+
"""))
|
| 25 |
+
|
| 26 |
+
# 创建 transactions 表
|
| 27 |
+
conn.execute(text("""
|
| 28 |
+
CREATE TABLE IF NOT EXISTS transactions (
|
| 29 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 30 |
+
account VARCHAR(255),
|
| 31 |
+
amount FLOAT,
|
| 32 |
+
transaction_type VARCHAR(50),
|
| 33 |
+
description TEXT,
|
| 34 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
| 35 |
+
)
|
| 36 |
+
"""))
|
| 37 |
+
|
| 38 |
+
conn.commit()
|
| 39 |
+
|
| 40 |
+
if __name__ == "__main__":
|
| 41 |
+
init_database()
|
| 42 |
+
print("数据库初始化完成")
|
models.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
# models.py
|
| 2 |
-
from pydantic import BaseModel
|
| 3 |
from typing import Optional, List
|
| 4 |
|
| 5 |
class SendCodeRequest(BaseModel):
|
|
@@ -57,7 +57,7 @@ class ItemCreate(BaseModel):
|
|
| 57 |
fullDesc: str
|
| 58 |
link: str
|
| 59 |
coverUrl: Optional[str] = None
|
| 60 |
-
imageUrls: Optional[List[str]] = []
|
| 61 |
author: str
|
| 62 |
price: int = 0
|
| 63 |
github_token: Optional[str] = None
|
|
@@ -68,7 +68,7 @@ class ItemUpdate(BaseModel):
|
|
| 68 |
fullDesc: Optional[str] = None
|
| 69 |
link: Optional[str] = None
|
| 70 |
coverUrl: Optional[str] = None
|
| 71 |
-
imageUrls: Optional[List[str]] = []
|
| 72 |
price: Optional[int] = None
|
| 73 |
github_token: Optional[str] = None
|
| 74 |
|
|
@@ -94,8 +94,6 @@ class InteractionToggle(BaseModel):
|
|
| 94 |
action_type: str
|
| 95 |
is_active: bool
|
| 96 |
|
| 97 |
-
# === 资金与钱包专有模型 ===
|
| 98 |
-
|
| 99 |
class RechargeRequest(BaseModel):
|
| 100 |
account: str
|
| 101 |
amount: int
|
|
|
|
| 1 |
# models.py
|
| 2 |
+
from pydantic import BaseModel
|
| 3 |
from typing import Optional, List
|
| 4 |
|
| 5 |
class SendCodeRequest(BaseModel):
|
|
|
|
| 57 |
fullDesc: str
|
| 58 |
link: str
|
| 59 |
coverUrl: Optional[str] = None
|
| 60 |
+
imageUrls: Optional[List[str]] = []
|
| 61 |
author: str
|
| 62 |
price: int = 0
|
| 63 |
github_token: Optional[str] = None
|
|
|
|
| 68 |
fullDesc: Optional[str] = None
|
| 69 |
link: Optional[str] = None
|
| 70 |
coverUrl: Optional[str] = None
|
| 71 |
+
imageUrls: Optional[List[str]] = []
|
| 72 |
price: Optional[int] = None
|
| 73 |
github_token: Optional[str] = None
|
| 74 |
|
|
|
|
| 94 |
action_type: str
|
| 95 |
is_active: bool
|
| 96 |
|
|
|
|
|
|
|
| 97 |
class RechargeRequest(BaseModel):
|
| 98 |
account: str
|
| 99 |
amount: int
|
nginx.conf
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 项目根目录下的 nginx.conf
|
| 2 |
+
events {
|
| 3 |
+
worker_connections 1024;
|
| 4 |
+
}
|
| 5 |
+
|
| 6 |
+
http {
|
| 7 |
+
include mime.types;
|
| 8 |
+
default_type application/octet-stream;
|
| 9 |
+
|
| 10 |
+
# 开启 Range 支持
|
| 11 |
+
range_resolver on;
|
| 12 |
+
range_buffer_size 16k;
|
| 13 |
+
|
| 14 |
+
# 静态文件服务配置
|
| 15 |
+
server {
|
| 16 |
+
listen 80;
|
| 17 |
+
server_name localhost;
|
| 18 |
+
|
| 19 |
+
# 配置静态文件目录
|
| 20 |
+
location /static/ {
|
| 21 |
+
root /path/to/your/project;
|
| 22 |
+
# 开启断点续传支持
|
| 23 |
+
add_header Accept-Ranges bytes;
|
| 24 |
+
# 启用缓存
|
| 25 |
+
expires 30d;
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
# 配置 API 代理
|
| 29 |
+
location /api/ {
|
| 30 |
+
proxy_pass http://localhost:5000; # 假设后端运行在5000端口
|
| 31 |
+
proxy_http_version 1.1;
|
| 32 |
+
proxy_set_header Upgrade $http_upgrade;
|
| 33 |
+
proxy_set_header Connection 'upgrade';
|
| 34 |
+
proxy_set_header Host $host;
|
| 35 |
+
proxy_cache_bypass $http_upgrade;
|
| 36 |
+
|
| 37 |
+
# 开启 Range 支持
|
| 38 |
+
proxy_set_header Range $http_range;
|
| 39 |
+
proxy_set_header If-Range $http_if_range;
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
# 配置文件下载
|
| 43 |
+
location /download/ {
|
| 44 |
+
root /path/to/your/project;
|
| 45 |
+
# 开启断点续传
|
| 46 |
+
add_header Accept-Ranges bytes;
|
| 47 |
+
# 设置缓存
|
| 48 |
+
expires 7d;
|
| 49 |
+
# 启用 gzip 压缩
|
| 50 |
+
gzip on;
|
| 51 |
+
gzip_types application/octet-stream;
|
| 52 |
+
}
|
| 53 |
+
}
|
| 54 |
+
}
|
router_items.py
CHANGED
|
@@ -1,177 +1,115 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
import time
|
| 4 |
-
import
|
| 5 |
-
import
|
| 6 |
-
import
|
| 7 |
-
from models import ItemCreate, ItemUpdate
|
| 8 |
|
| 9 |
router = APIRouter()
|
| 10 |
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
|
| 23 |
-
@router.
|
| 24 |
-
async def
|
| 25 |
-
|
| 26 |
-
comments_db = db.load_data("comments.json", default_data={})
|
| 27 |
|
| 28 |
-
#
|
| 29 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
|
| 49 |
-
@router.
|
| 50 |
-
async def
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
comments_db = db.load_data("comments.json", default_data={})
|
| 54 |
|
| 55 |
-
|
| 56 |
-
|
| 57 |
|
| 58 |
-
|
| 59 |
-
|
| 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 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 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.
|
| 146 |
-
async def
|
| 147 |
-
|
| 148 |
-
|
| 149 |
|
| 150 |
-
if
|
| 151 |
-
|
| 152 |
|
| 153 |
-
|
| 154 |
-
|
|
|
|
| 155 |
|
| 156 |
-
|
| 157 |
-
if
|
| 158 |
-
|
| 159 |
-
|
| 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 |
-
|
| 169 |
-
|
| 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="找不到该内容记录")
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import json
|
| 3 |
import time
|
| 4 |
+
from fastapi import APIRouter, HTTPException
|
| 5 |
+
from pydantic import BaseModel
|
| 6 |
+
from xss_filter import clean_html # 导入 XSS 过滤函数
|
|
|
|
| 7 |
|
| 8 |
router = APIRouter()
|
| 9 |
|
| 10 |
+
class Item(BaseModel):
|
| 11 |
+
name: str
|
| 12 |
+
description: str
|
| 13 |
+
fullDesc: str
|
| 14 |
+
author: str
|
| 15 |
+
tags: list
|
| 16 |
+
price: float
|
| 17 |
+
downloadCount: int
|
| 18 |
+
rating: float
|
| 19 |
+
thumbnail: str
|
| 20 |
+
repoUrl: str
|
| 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.post("/create_item")
|
| 62 |
+
async def create_item(item: Item):
|
| 63 |
+
items = load_data()
|
|
|
|
| 64 |
|
| 65 |
+
# XSS 过滤
|
| 66 |
+
item.fullDesc = clean_html(item.fullDesc)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.post("/update_item/{item_id}")
|
| 77 |
+
async def update_item(item_id: str, item: ItemUpdate):
|
| 78 |
+
items = load_data()
|
| 79 |
+
target_item = next((i for i in items if i["id"] == item_id), None)
|
|
|
|
| 80 |
|
| 81 |
+
if not target_item:
|
| 82 |
+
raise HTTPException(status_code=404, detail="Item not found")
|
| 83 |
|
| 84 |
+
# XSS 过滤
|
| 85 |
+
if item.fullDesc:
|
| 86 |
+
item.fullDesc = clean_html(item.fullDesc)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
|
| 88 |
+
# 更新字段
|
| 89 |
+
for key, value in item.dict(exclude_unset=True).items():
|
| 90 |
+
target_item[key] = value
|
| 91 |
+
|
| 92 |
+
target_item["updatedAt"] = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
|
| 93 |
+
|
| 94 |
+
save_data(items)
|
| 95 |
+
return {"status": "success", "message": "Item updated"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
|
| 97 |
+
@router.post("/purchase")
|
| 98 |
+
async def purchase(purchase_req: PurchaseRequest):
|
| 99 |
+
items = load_data()
|
| 100 |
+
item = next((i for i in items if i["id"] == purchase_req.item_id), None)
|
| 101 |
|
| 102 |
+
if not item:
|
| 103 |
+
raise HTTPException(status_code=404, detail="Item not found")
|
| 104 |
|
| 105 |
+
# 检查是否已购买
|
| 106 |
+
if purchase_req.account in item.get("purchasedBy", []):
|
| 107 |
+
raise HTTPException(status_code=400, detail="Item already purchased")
|
| 108 |
|
| 109 |
+
# 更新购买记录
|
| 110 |
+
if "purchasedBy" not in item:
|
| 111 |
+
item["purchasedBy"] = []
|
| 112 |
+
item["purchasedBy"].append(purchase_req.account)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
|
| 114 |
+
save_data(items)
|
| 115 |
+
return {"status": "success", "message": "Purchase successful"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
router_wallet.py
CHANGED
|
@@ -1,238 +1,149 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
from fastapi.responses import Response
|
| 4 |
-
from sqlalchemy.orm import Session
|
| 5 |
import time
|
| 6 |
-
import
|
| 7 |
import hashlib
|
| 8 |
-
import
|
| 9 |
-
from
|
| 10 |
-
from
|
| 11 |
-
from
|
| 12 |
-
|
|
|
|
| 13 |
|
| 14 |
router = APIRouter()
|
| 15 |
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 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 |
-
|
| 32 |
-
|
| 33 |
-
|
|
|
|
| 34 |
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 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 |
-
|
| 56 |
-
|
| 57 |
-
|
| 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 |
-
@
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
|
| 103 |
-
@
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
}
|
| 113 |
|
| 114 |
-
@router.post("/
|
| 115 |
-
async def
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
if not
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 127 |
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 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("/
|
| 162 |
-
async def
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 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.
|
| 202 |
-
async def
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 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"}
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import json
|
|
|
|
|
|
|
| 3 |
import time
|
| 4 |
+
import hmac
|
| 5 |
import hashlib
|
| 6 |
+
from fastapi import APIRouter, Request
|
| 7 |
+
from fastapi.responses import JSONResponse
|
| 8 |
+
from pydantic import BaseModel
|
| 9 |
+
from sqlalchemy import create_engine, text
|
| 10 |
+
from sqlalchemy.orm import sessionmaker
|
| 11 |
+
from functools import lru_cache # 新增导入
|
| 12 |
|
| 13 |
router = APIRouter()
|
| 14 |
|
| 15 |
+
# 数据库配置
|
| 16 |
+
DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///./test.db")
|
| 17 |
+
engine = create_engine(DATABASE_URL)
|
| 18 |
+
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
|
| 20 |
+
# 支付宝配置
|
| 21 |
+
ALIPAY_APP_ID = os.getenv("ALIPAY_APP_ID", "")
|
| 22 |
+
ALIPAY_PRIVATE_KEY = os.getenv("ALIPAY_PRIVATE_KEY", "")
|
| 23 |
+
ALIPAY_PUBLIC_KEY = os.getenv("ALIPAY_PUBLIC_KEY", "")
|
| 24 |
|
| 25 |
+
class WalletTransaction(BaseModel):
|
| 26 |
+
account: str
|
| 27 |
+
amount: float
|
| 28 |
+
transaction_type: str # 'deposit' or 'withdraw'
|
| 29 |
+
description: str
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
|
| 31 |
+
def verify_alipay_signature(params):
|
| 32 |
+
# 验证支付宝签名的逻辑
|
| 33 |
+
# 这里简化为示例
|
| 34 |
+
return True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
|
| 36 |
+
@lru_cache(maxsize=128) # 新增缓存装饰器
|
| 37 |
+
def get_user_balance(account: str):
|
| 38 |
+
"""获取用户余额(带缓存)"""
|
| 39 |
+
with SessionLocal() as session:
|
| 40 |
+
user = session.execute(
|
| 41 |
+
text("SELECT balance FROM users WHERE account = :account"),
|
| 42 |
+
{"account": account}
|
| 43 |
+
).fetchone()
|
| 44 |
+
|
| 45 |
+
if not user:
|
| 46 |
+
return 0
|
| 47 |
+
return user[0]
|
| 48 |
|
| 49 |
+
@lru_cache(maxsize=128) # 新增缓存装饰器
|
| 50 |
+
def check_idempotency(trade_no: str):
|
| 51 |
+
"""检查幂等性(带缓存)"""
|
| 52 |
+
with SessionLocal() as session:
|
| 53 |
+
exists = session.execute(
|
| 54 |
+
text("SELECT 1 FROM idempotency WHERE trade_no = :trade_no"),
|
| 55 |
+
{"trade_no": trade_no}
|
| 56 |
+
).fetchone()
|
| 57 |
+
return exists is not None
|
|
|
|
| 58 |
|
| 59 |
+
@router.post("/alipay_notify")
|
| 60 |
+
async def alipay_notify(request: Request):
|
| 61 |
+
params = await request.form()
|
| 62 |
+
|
| 63 |
+
# 验证签名
|
| 64 |
+
if not verify_alipay_signature(params):
|
| 65 |
+
return JSONResponse(content={"status": "error", "message": "Invalid signature"}, status_code=400)
|
| 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 |
+
if not user:
|
| 100 |
+
# 创建新用户
|
| 101 |
+
session.execute(
|
| 102 |
+
text("INSERT INTO users (account, balance) VALUES (:account, :balance)"),
|
| 103 |
+
{"account": account, "balance": amount}
|
| 104 |
+
)
|
| 105 |
+
else:
|
| 106 |
+
# 更新余额
|
| 107 |
+
session.execute(
|
| 108 |
+
text("UPDATE users SET balance = balance + :amount WHERE account = :account"),
|
| 109 |
+
{"amount": amount, "account": account}
|
| 110 |
+
)
|
| 111 |
|
| 112 |
+
# 记录交易
|
| 113 |
+
session.execute(
|
| 114 |
+
text("INSERT INTO transactions (account, amount, transaction_type, description) VALUES (:account, :amount, 'deposit', '支付宝充值')"),
|
| 115 |
+
{"account": account, "amount": amount}
|
| 116 |
+
)
|
| 117 |
+
session.commit()
|
| 118 |
+
|
| 119 |
+
# 清除缓存
|
| 120 |
+
get_user_balance.cache_clear()
|
| 121 |
+
check_idempotency.cache_clear()
|
| 122 |
+
|
| 123 |
+
return JSONResponse(content={"status": "success", "message": "Payment processed"})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
|
| 125 |
+
@router.post("/create_transaction")
|
| 126 |
+
async def create_transaction(transaction: WalletTransaction):
|
| 127 |
+
with SessionLocal() as session:
|
| 128 |
+
# 记录交易
|
| 129 |
+
session.execute(
|
| 130 |
+
text("INSERT INTO transactions (account, amount, transaction_type, description) VALUES (:account, :amount, :transaction_type, :description)"),
|
| 131 |
+
{
|
| 132 |
+
"account": transaction.account,
|
| 133 |
+
"amount": transaction.amount,
|
| 134 |
+
"transaction_type": transaction.transaction_type,
|
| 135 |
+
"description": transaction.description
|
| 136 |
+
}
|
| 137 |
+
)
|
| 138 |
+
session.commit()
|
| 139 |
+
|
| 140 |
+
# 清除缓存
|
| 141 |
+
get_user_balance.cache_clear()
|
| 142 |
+
|
| 143 |
+
return JSONResponse(content={"status": "success", "message": "Transaction created"})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 144 |
|
| 145 |
+
@router.get("/get_balance/{account}")
|
| 146 |
+
async def get_balance(account: str):
|
| 147 |
+
# 使用缓存获取余额
|
| 148 |
+
balance = get_user_balance(account)
|
| 149 |
+
return JSONResponse(content={"balance": balance})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
xss_filter.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import bleach
|
| 2 |
+
|
| 3 |
+
# 定义允许的标签和属性
|
| 4 |
+
ALLOWED_TAGS = ['p', 'img', 'a', 'br', 'b', 'i', 'u']
|
| 5 |
+
ALLOWED_ATTRS = {
|
| 6 |
+
'img': ['src', 'alt'],
|
| 7 |
+
'a': ['href', 'target']
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
def clean_html(html_content):
|
| 11 |
+
"""
|
| 12 |
+
清理 HTML 内容,防止 XSS 攻击
|
| 13 |
+
|
| 14 |
+
Args:
|
| 15 |
+
html_content (str): 需要清理的 HTML 内容
|
| 16 |
+
|
| 17 |
+
Returns:
|
| 18 |
+
str: 清理后的安全 HTML 内容
|
| 19 |
+
"""
|
| 20 |
+
return bleach.clean(
|
| 21 |
+
html_content,
|
| 22 |
+
tags=ALLOWED_TAGS,
|
| 23 |
+
attributes=ALLOWED_ATTRS,
|
| 24 |
+
strip=True
|
| 25 |
+
)
|