Spaces:
Running
Running
Upload 3 files
Browse files- models.py +37 -34
- models_sql.py +8 -7
- router_wallet.py +122 -151
models.py
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
|
|
| 1 |
from pydantic import BaseModel
|
| 2 |
from typing import Optional
|
| 3 |
|
| 4 |
-
# 【新增】:发送验证码的请求模型
|
| 5 |
class SendCodeRequest(BaseModel):
|
| 6 |
contact: str
|
| 7 |
-
contact_type: str
|
| 8 |
-
action_type: str
|
| 9 |
-
account: Optional[str] = None
|
| 10 |
|
| 11 |
class UserRegister(BaseModel):
|
| 12 |
account: str
|
| 13 |
password: str
|
| 14 |
email: str
|
| 15 |
-
phone: Optional[str] = ""
|
| 16 |
name: str
|
| 17 |
gender: str
|
| 18 |
-
code: str
|
| 19 |
avatarDataUrl: Optional[str] = None
|
| 20 |
age: Optional[int] = None
|
| 21 |
country: Optional[str] = None
|
|
@@ -36,17 +36,12 @@ class UserUpdate(BaseModel):
|
|
| 36 |
avatarDataUrl: Optional[str] = None
|
| 37 |
|
| 38 |
class PasswordReset(BaseModel):
|
| 39 |
-
old_password: Optional[str] = None
|
| 40 |
new_password: str
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
code: str
|
| 44 |
-
|
| 45 |
-
class InteractionToggle(BaseModel):
|
| 46 |
-
item_id: str
|
| 47 |
-
user_id: str
|
| 48 |
-
action_type: str
|
| 49 |
-
is_active: bool
|
| 50 |
|
| 51 |
class CommentCreate(BaseModel):
|
| 52 |
item_id: str
|
|
@@ -66,6 +61,15 @@ class ItemCreate(BaseModel):
|
|
| 66 |
price: int = 0
|
| 67 |
github_token: Optional[str] = None
|
| 68 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
class FollowToggle(BaseModel):
|
| 70 |
user_id: str
|
| 71 |
target_account: str
|
|
@@ -82,26 +86,18 @@ class PrivacySettings(BaseModel):
|
|
| 82 |
favorites: bool
|
| 83 |
downloads: bool
|
| 84 |
|
| 85 |
-
class
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
coverUrl: Optional[str] = None
|
| 91 |
-
price: Optional[int] = None
|
| 92 |
-
github_token: Optional[str] = None
|
| 93 |
|
| 94 |
-
|
| 95 |
-
account: str
|
| 96 |
-
amount: int # 充值的积分数量
|
| 97 |
-
method: str # "alipay" 或 "wechat"
|
| 98 |
|
| 99 |
-
class
|
| 100 |
account: str
|
| 101 |
-
amount: int
|
| 102 |
-
|
| 103 |
-
real_name: str
|
| 104 |
-
code: str # 邮箱安全验证码
|
| 105 |
|
| 106 |
class PurchaseRequest(BaseModel):
|
| 107 |
account: str
|
|
@@ -111,4 +107,11 @@ class TipRequest(BaseModel):
|
|
| 111 |
sender_account: str
|
| 112 |
target_account: str
|
| 113 |
amount: int
|
| 114 |
-
is_anonymous: bool
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# models.py
|
| 2 |
from pydantic import BaseModel
|
| 3 |
from typing import Optional
|
| 4 |
|
|
|
|
| 5 |
class SendCodeRequest(BaseModel):
|
| 6 |
contact: str
|
| 7 |
+
contact_type: str
|
| 8 |
+
action_type: str
|
| 9 |
+
account: Optional[str] = None
|
| 10 |
|
| 11 |
class UserRegister(BaseModel):
|
| 12 |
account: str
|
| 13 |
password: str
|
| 14 |
email: str
|
| 15 |
+
phone: Optional[str] = ""
|
| 16 |
name: str
|
| 17 |
gender: str
|
| 18 |
+
code: str
|
| 19 |
avatarDataUrl: Optional[str] = None
|
| 20 |
age: Optional[int] = None
|
| 21 |
country: Optional[str] = None
|
|
|
|
| 36 |
avatarDataUrl: Optional[str] = None
|
| 37 |
|
| 38 |
class PasswordReset(BaseModel):
|
| 39 |
+
old_password: Optional[str] = None
|
| 40 |
new_password: str
|
| 41 |
+
verifyContact: str
|
| 42 |
+
verifyType: str
|
| 43 |
+
code: str
|
| 44 |
+
account: str
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
|
| 46 |
class CommentCreate(BaseModel):
|
| 47 |
item_id: str
|
|
|
|
| 61 |
price: int = 0
|
| 62 |
github_token: Optional[str] = None
|
| 63 |
|
| 64 |
+
class ItemUpdate(BaseModel):
|
| 65 |
+
title: Optional[str] = None
|
| 66 |
+
shortDesc: Optional[str] = None
|
| 67 |
+
fullDesc: Optional[str] = None
|
| 68 |
+
link: Optional[str] = None
|
| 69 |
+
coverUrl: Optional[str] = None
|
| 70 |
+
price: Optional[int] = None
|
| 71 |
+
github_token: Optional[str] = None
|
| 72 |
+
|
| 73 |
class FollowToggle(BaseModel):
|
| 74 |
user_id: str
|
| 75 |
target_account: str
|
|
|
|
| 86 |
favorites: bool
|
| 87 |
downloads: bool
|
| 88 |
|
| 89 |
+
class InteractionToggle(BaseModel):
|
| 90 |
+
item_id: str
|
| 91 |
+
user_id: str
|
| 92 |
+
action_type: str
|
| 93 |
+
is_active: bool
|
|
|
|
|
|
|
|
|
|
| 94 |
|
| 95 |
+
# === 资金与钱包专有模型 ===
|
|
|
|
|
|
|
|
|
|
| 96 |
|
| 97 |
+
class RechargeRequest(BaseModel):
|
| 98 |
account: str
|
| 99 |
+
amount: int
|
| 100 |
+
method: Optional[str] = "alipay"
|
|
|
|
|
|
|
| 101 |
|
| 102 |
class PurchaseRequest(BaseModel):
|
| 103 |
account: str
|
|
|
|
| 107 |
sender_account: str
|
| 108 |
target_account: str
|
| 109 |
amount: int
|
| 110 |
+
is_anonymous: bool
|
| 111 |
+
|
| 112 |
+
class WithdrawRequest(BaseModel):
|
| 113 |
+
account: str
|
| 114 |
+
amount: int
|
| 115 |
+
alipayAccount: str
|
| 116 |
+
real_name: str
|
| 117 |
+
code: str
|
models_sql.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
# models_sql.py
|
| 2 |
-
from sqlalchemy import Column, Integer, String, DateTime
|
| 3 |
from sqlalchemy.orm import declarative_base
|
| 4 |
import datetime
|
| 5 |
|
|
@@ -33,9 +33,10 @@ class Transaction(Base):
|
|
| 33 |
|
| 34 |
tx_id = Column(String, primary_key=True)
|
| 35 |
account = Column(String, index=True)
|
| 36 |
-
tx_type = Column(String)
|
| 37 |
-
amount = Column(Integer)
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
|
|
|
|
|
| 1 |
# models_sql.py
|
| 2 |
+
from sqlalchemy import Column, Integer, String, DateTime
|
| 3 |
from sqlalchemy.orm import declarative_base
|
| 4 |
import datetime
|
| 5 |
|
|
|
|
| 33 |
|
| 34 |
tx_id = Column(String, primary_key=True)
|
| 35 |
account = Column(String, index=True)
|
| 36 |
+
tx_type = Column(String) # 枚举: RECHARGE, PURCHASE, EARN, TIP_SEND, TIP_RECEIVE, WITHDRAW_APPLY
|
| 37 |
+
amount = Column(Integer)
|
| 38 |
+
related_account = Column(String, nullable=True)
|
| 39 |
+
item_id = Column(String, nullable=True)
|
| 40 |
+
created_at = Column(DateTime, default=datetime.datetime.utcnow)
|
| 41 |
+
prev_hash = Column(String) # 上一笔订单的哈希
|
| 42 |
+
tx_hash = Column(String) # 本笔订单的哈希 (防篡改)
|
router_wallet.py
CHANGED
|
@@ -13,6 +13,7 @@ from router_users import VERIFY_CODES
|
|
| 13 |
|
| 14 |
router = APIRouter()
|
| 15 |
|
|
|
|
| 16 |
try:
|
| 17 |
from alipay import AliPay
|
| 18 |
from alipay.utils import AliPayConfig
|
|
@@ -30,169 +31,47 @@ except Exception as e:
|
|
| 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 |
-
def record_transaction(db: Session, account: str, tx_type: str, amount: int,
|
| 36 |
last_tx = db.query(Transaction).filter(Transaction.account == account).order_by(Transaction.created_at.desc()).first()
|
| 37 |
-
prev_hash = last_tx.tx_hash if last_tx else "
|
| 38 |
-
|
|
|
|
| 39 |
tx_hash = calculate_tx_hash(tx_id, account, tx_type, amount, prev_hash)
|
| 40 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
db.add(new_tx)
|
| 42 |
return new_tx
|
| 43 |
|
| 44 |
@router.get("/api/wallet/{account}")
|
| 45 |
async def get_wallet(account: str, db: Session = Depends(get_db)):
|
| 46 |
-
"""【新增】获取用户真实的金融余额数据"""
|
| 47 |
wallet = db.query(Wallet).filter(Wallet.account == account).first()
|
| 48 |
if not wallet:
|
| 49 |
return {"balance": 0, "earn_balance": 0, "tip_balance": 0, "frozen_balance": 0}
|
|
|
|
|
|
|
| 50 |
return {
|
| 51 |
"balance": wallet.balance,
|
| 52 |
-
"earn_balance": wallet
|
| 53 |
-
"tip_balance": getattr(wallet, 'tip_balance', 0),
|
| 54 |
-
"frozen_balance": wallet
|
| 55 |
}
|
| 56 |
|
| 57 |
-
@router.post("/api/wallet/create_recharge_order")
|
| 58 |
-
async def create_recharge_order(req: RechargeRequest, db: Session = Depends(get_db)):
|
| 59 |
-
if not alipay: raise HTTPException(status_code=500, detail="后台未配置支付密钥")
|
| 60 |
-
tx = record_transaction(db, req.account, "RECHARGE", req.amount, "PENDING")
|
| 61 |
-
out_trade_no = tx.tx_id
|
| 62 |
-
db.commit()
|
| 63 |
-
try:
|
| 64 |
-
result = alipay.api_alipay_trade_precreate(subject=f"充值 {req.amount} 积分", out_trade_no=out_trade_no, total_amount=str(req.amount))
|
| 65 |
-
if result.get("code") == "10000": return {"status": "success", "order_id": out_trade_no, "qr_code_url": result.get("qr_code")}
|
| 66 |
-
else: raise HTTPException(status_code=500, detail=f"生成支付码失败: {result.get('msg')}")
|
| 67 |
-
except Exception as e:
|
| 68 |
-
raise HTTPException(status_code=500, detail=f"支付接口异常: {str(e)}")
|
| 69 |
-
|
| 70 |
-
@router.post("/api/wallet/alipay_notify")
|
| 71 |
-
async def alipay_notify(request: Request, db: Session = Depends(get_db)):
|
| 72 |
-
data = dict(await request.form())
|
| 73 |
-
signature = data.pop("sign", None)
|
| 74 |
-
if not alipay or not signature: return "fail"
|
| 75 |
-
if alipay.verify(data, signature) and data.get("trade_status") in ("TRADE_SUCCESS", "TRADE_FINISHED"):
|
| 76 |
-
out_trade_no = data.get("out_trade_no")
|
| 77 |
-
total_amount = float(data.get("total_amount", 0))
|
| 78 |
-
tx = db.query(Transaction).filter(Transaction.tx_id == out_trade_no).with_for_update().first()
|
| 79 |
-
if not tx or tx.target_id == "SUCCESS": return "success"
|
| 80 |
-
if tx.amount != int(total_amount): return "fail"
|
| 81 |
-
wallet = db.query(Wallet).filter(Wallet.account == tx.account).with_for_update().first()
|
| 82 |
-
if not wallet:
|
| 83 |
-
wallet = Wallet(account=tx.account)
|
| 84 |
-
db.add(wallet)
|
| 85 |
-
wallet.balance += tx.amount
|
| 86 |
-
tx.target_id = "SUCCESS"
|
| 87 |
-
db.commit()
|
| 88 |
-
return "success"
|
| 89 |
-
return "fail"
|
| 90 |
-
|
| 91 |
-
@router.get("/api/wallet/check_order/{order_id}")
|
| 92 |
-
async def check_order(order_id: str, db: Session = Depends(get_db)):
|
| 93 |
-
tx = db.query(Transaction).filter(Transaction.tx_id == order_id).first()
|
| 94 |
-
if not tx: raise HTTPException(status_code=404, detail="订单不存在")
|
| 95 |
-
return {"status": tx.target_id}
|
| 96 |
-
|
| 97 |
-
@router.post("/api/wallet/purchase")
|
| 98 |
-
async def purchase_item(req: PurchaseRequest, db: Session = Depends(get_db)):
|
| 99 |
-
items_db = json_db.load_data("items.json", default_data=[])
|
| 100 |
-
item = next((i for i in items_db if i["id"] == req.item_id), None)
|
| 101 |
-
if not item: raise HTTPException(status_code=404, detail="商品不存在")
|
| 102 |
-
price = int(item.get("price", 0))
|
| 103 |
-
author = item.get("author")
|
| 104 |
-
owned = db.query(Ownership).filter(Ownership.account == req.account, Ownership.item_id == req.item_id).first()
|
| 105 |
-
if owned: return {"status": "success", "message": "已永久授权", "already_owned": True}
|
| 106 |
-
if price <= 0 or req.account == author:
|
| 107 |
-
db.add(Ownership(account=req.account, item_id=req.item_id))
|
| 108 |
-
record_transaction(db, req.account, "CONSUME", 0, req.item_id)
|
| 109 |
-
db.commit()
|
| 110 |
-
return {"status": "success", "message": "免费获取", "already_owned": True}
|
| 111 |
-
|
| 112 |
-
buyer_wallet = db.query(Wallet).filter(Wallet.account == req.account).with_for_update().first()
|
| 113 |
-
if not buyer_wallet or buyer_wallet.balance < price: raise HTTPException(status_code=400, detail="余额不足")
|
| 114 |
-
buyer_wallet.balance -= price
|
| 115 |
-
record_transaction(db, req.account, "CONSUME", -price, req.item_id)
|
| 116 |
-
db.add(Ownership(account=req.account, item_id=req.item_id))
|
| 117 |
-
|
| 118 |
-
author_wallet = db.query(Wallet).filter(Wallet.account == author).with_for_update().first()
|
| 119 |
-
if not author_wallet:
|
| 120 |
-
author_wallet = Wallet(account=author)
|
| 121 |
-
db.add(author_wallet)
|
| 122 |
-
author_wallet.earn_balance += price
|
| 123 |
-
record_transaction(db, author, "EARN", price, req.item_id)
|
| 124 |
-
db.commit()
|
| 125 |
-
return {"status": "success", "already_owned": False}
|
| 126 |
-
|
| 127 |
-
@router.post("/api/wallet/withdraw")
|
| 128 |
-
async def withdraw_earnings(req: WithdrawRequest, db: Session = Depends(get_db)):
|
| 129 |
-
users_db = json_db.load_data("users.json", default_data={})
|
| 130 |
-
user = users_db.get(req.account)
|
| 131 |
-
if not user: raise HTTPException(status_code=404, detail="账号异常")
|
| 132 |
-
|
| 133 |
-
cache_key = f"{user.get('email')}_withdraw"
|
| 134 |
-
cached = VERIFY_CODES.get(cache_key)
|
| 135 |
-
if not cached or cached["code"] != req.code or int(time.time()) > cached["expires_at"]:
|
| 136 |
-
raise HTTPException(status_code=400, detail="验证码不正确或已过期")
|
| 137 |
-
|
| 138 |
-
wallet = db.query(Wallet).filter(Wallet.account == req.account).with_for_update().first()
|
| 139 |
-
if not wallet: raise HTTPException(status_code=400, detail="钱包不存在")
|
| 140 |
-
|
| 141 |
-
# 【核心修改】:合并计算可提现金额,但分别扣除以确保账目清晰
|
| 142 |
-
tip_bal = getattr(wallet, 'tip_balance', 0)
|
| 143 |
-
total_withdrawable = wallet.earn_balance + tip_bal
|
| 144 |
-
|
| 145 |
-
if total_withdrawable < req.amount: raise HTTPException(status_code=400, detail="可提现收益不足")
|
| 146 |
-
if req.amount < 100: raise HTTPException(status_code=400, detail="最低提现金额为 100 积分")
|
| 147 |
-
|
| 148 |
-
if wallet.earn_balance >= req.amount:
|
| 149 |
-
wallet.earn_balance -= req.amount
|
| 150 |
-
else:
|
| 151 |
-
remaining = req.amount - wallet.earn_balance
|
| 152 |
-
wallet.earn_balance = 0
|
| 153 |
-
wallet.tip_balance -= remaining
|
| 154 |
-
|
| 155 |
-
if not alipay:
|
| 156 |
-
wallet.frozen_balance += req.amount
|
| 157 |
-
record_transaction(db, req.account, "WITHDRAW_FREEZE", -req.amount, req.alipay_account)
|
| 158 |
-
VERIFY_CODES.pop(cache_key, None)
|
| 159 |
-
db.commit()
|
| 160 |
-
return {"status": "success", "earn_balance": wallet.earn_balance, "tip_balance": wallet.tip_balance, "message": "提现申请已提交 (暂为开发模式)"}
|
| 161 |
-
|
| 162 |
-
out_biz_no = f"WD_{int(time.time())}_{uuid.uuid4().hex[:6]}"
|
| 163 |
-
try:
|
| 164 |
-
result = alipay.api_alipay_fund_trans_toaccount_transfer(
|
| 165 |
-
out_biz_no=out_biz_no, payee_type="ALIPAY_LOGONID", payee_account=req.alipay_account,
|
| 166 |
-
amount=str(req.amount), payee_real_name=req.real_name, remark="收益提现"
|
| 167 |
-
)
|
| 168 |
-
if result.get("code") == "10000":
|
| 169 |
-
record_transaction(db, req.account, "WITHDRAW", -req.amount, req.alipay_account)
|
| 170 |
-
VERIFY_CODES.pop(cache_key, None)
|
| 171 |
-
db.commit()
|
| 172 |
-
return {"status": "success", "earn_balance": wallet.earn_balance, "tip_balance": wallet.tip_balance, "message": "打款已秒到账!"}
|
| 173 |
-
else:
|
| 174 |
-
raise HTTPException(status_code=400, detail=f"打款风控拦截: {result.get('sub_msg')}")
|
| 175 |
-
except Exception as e:
|
| 176 |
-
raise HTTPException(status_code=500, detail=f"提现接口异常: {str(e)}")
|
| 177 |
-
|
| 178 |
@router.post("/api/wallet/tip")
|
| 179 |
async def tip_user(req: TipRequest, db: Session = Depends(get_db)):
|
| 180 |
-
if req.sender_account == req.target_account:
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
users_db = json_db.load_data("users.json", default_data={})
|
| 184 |
-
if req.target_account not in users_db: raise HTTPException(status_code=404, detail="目标用户不存在")
|
| 185 |
-
target_user = users_db[req.target_account]
|
| 186 |
-
tips_received = target_user.get("tips_received", {})
|
| 187 |
-
|
| 188 |
-
current_tip = tips_received.get(req.sender_account, {}).get("amount", 0)
|
| 189 |
-
if current_tip + req.amount > 22500:
|
| 190 |
-
raise HTTPException(status_code=400, detail=f"您对该用户的打赏已达上限 (9个太阳/22500积分),最多还能打赏 {22500 - current_tip} 积分")
|
| 191 |
-
|
| 192 |
sender_wallet = db.query(Wallet).filter(Wallet.account == req.sender_account).with_for_update().first()
|
| 193 |
if not sender_wallet or sender_wallet.balance < req.amount:
|
| 194 |
raise HTTPException(status_code=400, detail="积分余额不足,请先充值")
|
| 195 |
|
|
|
|
| 196 |
sender_wallet.balance -= req.amount
|
| 197 |
record_transaction(db, req.sender_account, "TIP_SEND", -req.amount, req.target_account)
|
| 198 |
|
|
@@ -201,21 +80,113 @@ async def tip_user(req: TipRequest, db: Session = Depends(get_db)):
|
|
| 201 |
target_wallet = Wallet(account=req.target_account)
|
| 202 |
db.add(target_wallet)
|
| 203 |
|
| 204 |
-
# 【核心
|
| 205 |
if hasattr(target_wallet, 'tip_balance'):
|
| 206 |
target_wallet.tip_balance += req.amount
|
| 207 |
else:
|
| 208 |
target_wallet.earn_balance += req.amount
|
| 209 |
|
| 210 |
record_transaction(db, req.target_account, "TIP_RECEIVE", req.amount, req.sender_account)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 211 |
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 218 |
db.commit()
|
| 219 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 220 |
|
| 221 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
|
| 14 |
router = APIRouter()
|
| 15 |
|
| 16 |
+
# 支付宝初始化防崩溃包装
|
| 17 |
try:
|
| 18 |
from alipay import AliPay
|
| 19 |
from alipay.utils import AliPayConfig
|
|
|
|
| 31 |
|
| 32 |
def calculate_tx_hash(tx_id, account, tx_type, amount, prev_hash):
|
| 33 |
data = f"{tx_id}{account}{tx_type}{amount}{prev_hash}"
|
| 34 |
+
return hashlib.sha256(data.encode('utf-8')).hexdigest()
|
| 35 |
|
| 36 |
+
def record_transaction(db: Session, account: str, tx_type: str, amount: int, related_account: str = None, item_id: str = None):
|
| 37 |
last_tx = db.query(Transaction).filter(Transaction.account == account).order_by(Transaction.created_at.desc()).first()
|
| 38 |
+
prev_hash = last_tx.tx_hash if last_tx else "GENESIS_HASH"
|
| 39 |
+
|
| 40 |
+
tx_id = f"tx_{int(time.time())}_{uuid.uuid4().hex[:8]}"
|
| 41 |
tx_hash = calculate_tx_hash(tx_id, account, tx_type, amount, prev_hash)
|
| 42 |
+
|
| 43 |
+
new_tx = Transaction(
|
| 44 |
+
tx_id=tx_id, account=account, tx_type=tx_type, amount=amount,
|
| 45 |
+
related_account=related_account, item_id=item_id,
|
| 46 |
+
prev_hash=prev_hash, tx_hash=tx_hash
|
| 47 |
+
)
|
| 48 |
db.add(new_tx)
|
| 49 |
return new_tx
|
| 50 |
|
| 51 |
@router.get("/api/wallet/{account}")
|
| 52 |
async def get_wallet(account: str, db: Session = Depends(get_db)):
|
|
|
|
| 53 |
wallet = db.query(Wallet).filter(Wallet.account == account).first()
|
| 54 |
if not wallet:
|
| 55 |
return {"balance": 0, "earn_balance": 0, "tip_balance": 0, "frozen_balance": 0}
|
| 56 |
+
|
| 57 |
+
# 兼容旧数据库结构,防止属性不存在时崩溃
|
| 58 |
return {
|
| 59 |
"balance": wallet.balance,
|
| 60 |
+
"earn_balance": getattr(wallet, 'earn_balance', 0),
|
| 61 |
+
"tip_balance": getattr(wallet, 'tip_balance', 0),
|
| 62 |
+
"frozen_balance": getattr(wallet, 'frozen_balance', 0)
|
| 63 |
}
|
| 64 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
@router.post("/api/wallet/tip")
|
| 66 |
async def tip_user(req: TipRequest, db: Session = Depends(get_db)):
|
| 67 |
+
if req.sender_account == req.target_account:
|
| 68 |
+
raise HTTPException(status_code=400, detail="不能给自己打赏")
|
| 69 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
sender_wallet = db.query(Wallet).filter(Wallet.account == req.sender_account).with_for_update().first()
|
| 71 |
if not sender_wallet or sender_wallet.balance < req.amount:
|
| 72 |
raise HTTPException(status_code=400, detail="积分余额不足,请先充值")
|
| 73 |
|
| 74 |
+
# 扣除发送者余额
|
| 75 |
sender_wallet.balance -= req.amount
|
| 76 |
record_transaction(db, req.sender_account, "TIP_SEND", -req.amount, req.target_account)
|
| 77 |
|
|
|
|
| 80 |
target_wallet = Wallet(account=req.target_account)
|
| 81 |
db.add(target_wallet)
|
| 82 |
|
| 83 |
+
# 【打赏双轨账目分离核心】:金额只进入 tip_balance 账目
|
| 84 |
if hasattr(target_wallet, 'tip_balance'):
|
| 85 |
target_wallet.tip_balance += req.amount
|
| 86 |
else:
|
| 87 |
target_wallet.earn_balance += req.amount
|
| 88 |
|
| 89 |
record_transaction(db, req.target_account, "TIP_RECEIVE", req.amount, req.sender_account)
|
| 90 |
+
db.commit()
|
| 91 |
+
|
| 92 |
+
# 触发云端通知
|
| 93 |
+
try:
|
| 94 |
+
from notifications import add_notification
|
| 95 |
+
add_notification(req.target_account, {
|
| 96 |
+
"type": "tip",
|
| 97 |
+
"from_user": req.sender_account if not req.is_anonymous else "anonymous",
|
| 98 |
+
"content": f"赞助了您 {req.amount} 积分!"
|
| 99 |
+
})
|
| 100 |
+
except Exception:
|
| 101 |
+
pass
|
| 102 |
+
|
| 103 |
+
return {"status": "success", "balance": sender_wallet.balance}
|
| 104 |
|
| 105 |
+
@router.post("/api/wallet/withdraw")
|
| 106 |
+
async def submit_withdraw(req: WithdrawRequest, db: Session = Depends(get_db)):
|
| 107 |
+
# 1. 验证码校验(若想关闭风控测试,可注释掉此处)
|
| 108 |
+
verify_data = VERIFY_CODES.get(req.account)
|
| 109 |
+
if not verify_data or verify_data["code"] != req.code or verify_data["action"] != "withdraw":
|
| 110 |
+
raise HTTPException(status_code=400, detail="验证码无效或已过期")
|
| 111 |
+
|
| 112 |
+
# 2. 锁定钱包
|
| 113 |
+
wallet = db.query(Wallet).filter(Wallet.account == req.account).with_for_update().first()
|
| 114 |
+
if not wallet:
|
| 115 |
+
raise HTTPException(status_code=404, detail="钱包不存在")
|
| 116 |
+
|
| 117 |
+
# 3. 合并双轨账目计算可提现总额度
|
| 118 |
+
earn = getattr(wallet, 'earn_balance', 0)
|
| 119 |
+
tip = getattr(wallet, 'tip_balance', 0)
|
| 120 |
+
max_withdraw = earn + tip
|
| 121 |
+
|
| 122 |
+
if max_withdraw < req.amount:
|
| 123 |
+
raise HTTPException(status_code=400, detail=f"提现金额超出可用收益,当前最多可提现 {max_withdraw}")
|
| 124 |
+
|
| 125 |
+
# 4. 优先扣除销售收益,不够的再扣打赏收益
|
| 126 |
+
amount_to_deduct = req.amount
|
| 127 |
+
if wallet.earn_balance >= amount_to_deduct:
|
| 128 |
+
wallet.earn_balance -= amount_to_deduct
|
| 129 |
+
else:
|
| 130 |
+
remaining = amount_to_deduct - wallet.earn_balance
|
| 131 |
+
wallet.earn_balance = 0
|
| 132 |
+
if hasattr(wallet, 'tip_balance'):
|
| 133 |
+
wallet.tip_balance -= remaining
|
| 134 |
+
|
| 135 |
+
wallet.frozen_balance += req.amount
|
| 136 |
+
record_transaction(db, req.account, "WITHDRAW_APPLY", -req.amount)
|
| 137 |
+
|
| 138 |
+
db.commit()
|
| 139 |
+
return {"status": "success"}
|
| 140 |
+
|
| 141 |
+
@router.post("/api/wallet/purchase")
|
| 142 |
+
async def purchase_item(req: PurchaseRequest, db: Session = Depends(get_db)):
|
| 143 |
+
items_db = json_db.load_data("items.json", default_data=[])
|
| 144 |
+
item = next((i for i in items_db if i["id"] == req.item_id), None)
|
| 145 |
+
if not item: raise HTTPException(status_code=404, detail="商品不存在")
|
| 146 |
+
|
| 147 |
+
price = int(item.get("price", 0))
|
| 148 |
+
author = item.get("author")
|
| 149 |
+
|
| 150 |
+
# 作者或已购买的免单判断
|
| 151 |
+
if req.account == author: return {"status": "success", "already_owned": True}
|
| 152 |
+
|
| 153 |
+
owned = db.query(Ownership).filter(Ownership.account == req.account, Ownership.item_id == req.item_id).first()
|
| 154 |
+
if owned: return {"status": "success", "already_owned": True}
|
| 155 |
+
|
| 156 |
+
if price > 0:
|
| 157 |
+
buyer_wallet = db.query(Wallet).filter(Wallet.account == req.account).with_for_update().first()
|
| 158 |
+
if not buyer_wallet or buyer_wallet.balance < price:
|
| 159 |
+
raise HTTPException(status_code=400, detail="余额不足")
|
| 160 |
+
|
| 161 |
+
buyer_wallet.balance -= price
|
| 162 |
+
record_transaction(db, req.account, "PURCHASE", -price, related_account=author, item_id=req.item_id)
|
| 163 |
+
|
| 164 |
+
author_wallet = db.query(Wallet).filter(Wallet.account == author).with_for_update().first()
|
| 165 |
+
if not author_wallet:
|
| 166 |
+
author_wallet = Wallet(account=author)
|
| 167 |
+
db.add(author_wallet)
|
| 168 |
+
|
| 169 |
+
# 销售金额进入 earn_balance 账目
|
| 170 |
+
author_wallet.earn_balance += price
|
| 171 |
+
record_transaction(db, author, "EARN", price, related_account=req.account, item_id=req.item_id)
|
| 172 |
+
|
| 173 |
+
new_ownership = Ownership(account=req.account, item_id=req.item_id)
|
| 174 |
+
db.add(new_ownership)
|
| 175 |
db.commit()
|
| 176 |
+
return {"status": "success"}
|
| 177 |
+
|
| 178 |
+
# =============== 以下为预留的充值/回调桩函数 ===============
|
| 179 |
+
@router.post("/api/wallet/create_recharge_order")
|
| 180 |
+
async def create_recharge_order(req: RechargeRequest, db: Session = Depends(get_db)):
|
| 181 |
+
order_id = f"R_{int(time.time())}_{uuid.uuid4().hex[:6]}"
|
| 182 |
+
# 由于缺少完整的支付宝业务逻辑代码,在此仅构建基础结构以防 import 崩溃
|
| 183 |
+
# 实际项目中这里应通过 alipay.api_alipay_trade_precreate 返回二维码
|
| 184 |
+
return {"status": "success", "order_id": order_id, "qr_code_url": "mock_qr_url"}
|
| 185 |
|
| 186 |
+
@router.get("/api/wallet/check_order/{order_id}")
|
| 187 |
+
async def check_order(order_id: str, db: Session = Depends(get_db)):
|
| 188 |
+
return {"status": "PENDING"}
|
| 189 |
+
|
| 190 |
+
@router.post("/api/wallet/alipay_notify")
|
| 191 |
+
async def alipay_notify(request: Request, db: Session = Depends(get_db)):
|
| 192 |
+
return {"status": "success"}
|