ZHIWEI666 commited on
Commit
0be9501
·
verified ·
1 Parent(s): 613a50c

Upload 17 files

Browse files
Files changed (7) hide show
  1. idempotency_table.sql +4 -0
  2. init_db.py +42 -0
  3. models.py +3 -5
  4. nginx.conf +54 -0
  5. router_items.py +99 -161
  6. router_wallet.py +134 -223
  7. 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, validator
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
- # 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="找不到该内容记录")
 
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
- # 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"}
 
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
+ )