ZHIWEI666 commited on
Commit
4fa652c
·
verified ·
1 Parent(s): e3b0423

Upload 14 files

Browse files
Files changed (3) hide show
  1. requirements.txt +1 -2
  2. router_items.py +161 -99
  3. 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
- 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"}
 
 
 
 
 
 
 
 
 
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
- 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})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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"}