Spaces:
Running
Running
Upload 2 files
Browse files- models_sql.py +10 -12
- router_wallet.py +60 -118
models_sql.py
CHANGED
|
@@ -11,14 +11,15 @@ class Wallet(Base):
|
|
| 11 |
|
| 12 |
account = Column(String, primary_key=True, index=True)
|
| 13 |
balance = Column(Integer, default=0) # 用于消费的余额 (充值获得)
|
| 14 |
-
earn_balance = Column(Integer, default=0) # 创作者收益余额 (别人购买获得)
|
|
|
|
| 15 |
frozen_balance = Column(Integer, default=0) # 提现审核冻结中的余额
|
| 16 |
|
| 17 |
# 乐观锁版本号,防止并发扣款被击穿
|
| 18 |
version = Column(Integer, default=1)
|
| 19 |
|
| 20 |
class Ownership(Base):
|
| 21 |
-
"""资源所有权表
|
| 22 |
__tablename__ = "ownerships"
|
| 23 |
|
| 24 |
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
@@ -27,17 +28,14 @@ class Ownership(Base):
|
|
| 27 |
purchased_at = Column(DateTime, default=datetime.datetime.utcnow)
|
| 28 |
|
| 29 |
class Transaction(Base):
|
| 30 |
-
"""交易流水表
|
| 31 |
__tablename__ = "transactions"
|
| 32 |
|
| 33 |
-
tx_id = Column(String, primary_key=True)
|
| 34 |
account = Column(String, index=True)
|
| 35 |
-
tx_type = Column(String)
|
| 36 |
-
amount = Column(Integer)
|
| 37 |
-
target_id = Column(String, nullable=True)
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
prev_hash = Column(String, nullable=True) # 上一笔交易的哈希值
|
| 41 |
-
tx_hash = Column(String) # 当前交易哈希值
|
| 42 |
-
|
| 43 |
created_at = Column(DateTime, default=datetime.datetime.utcnow)
|
|
|
|
| 11 |
|
| 12 |
account = Column(String, primary_key=True, index=True)
|
| 13 |
balance = Column(Integer, default=0) # 用于消费的余额 (充值获得)
|
| 14 |
+
earn_balance = Column(Integer, default=0) # 创作者销售收益余额 (别人购买获得)
|
| 15 |
+
tip_balance = Column(Integer, default=0) # 【新增】创作者打赏收益余额 (粉丝赞助获得)
|
| 16 |
frozen_balance = Column(Integer, default=0) # 提现审核冻结中的余额
|
| 17 |
|
| 18 |
# 乐观锁版本号,防止并发扣款被击穿
|
| 19 |
version = Column(Integer, default=1)
|
| 20 |
|
| 21 |
class Ownership(Base):
|
| 22 |
+
"""资源所有权表"""
|
| 23 |
__tablename__ = "ownerships"
|
| 24 |
|
| 25 |
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
|
|
| 28 |
purchased_at = Column(DateTime, default=datetime.datetime.utcnow)
|
| 29 |
|
| 30 |
class Transaction(Base):
|
| 31 |
+
"""交易流水表"""
|
| 32 |
__tablename__ = "transactions"
|
| 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 |
+
target_id = Column(String, nullable=True)
|
| 39 |
+
prev_hash = Column(String, nullable=True)
|
| 40 |
+
tx_hash = Column(String)
|
|
|
|
|
|
|
|
|
|
| 41 |
created_at = Column(DateTime, default=datetime.datetime.utcnow)
|
router_wallet.py
CHANGED
|
@@ -7,16 +7,12 @@ import hashlib
|
|
| 7 |
import os
|
| 8 |
from database_sql import get_db
|
| 9 |
from models_sql import Wallet, Transaction, Ownership
|
| 10 |
-
# 【修复 1】:在这里补充导入了 TipRequest,防止云端崩溃!
|
| 11 |
from models import RechargeRequest, WithdrawRequest, PurchaseRequest, TipRequest
|
| 12 |
import 数据库连接 as json_db
|
| 13 |
from router_users import VERIFY_CODES
|
| 14 |
|
| 15 |
router = APIRouter()
|
| 16 |
|
| 17 |
-
# =======================================================
|
| 18 |
-
# 🏦 支付宝 SDK 初始化 (带防崩溃保护)
|
| 19 |
-
# =======================================================
|
| 20 |
try:
|
| 21 |
from alipay import AliPay
|
| 22 |
from alipay.utils import AliPayConfig
|
|
@@ -26,159 +22,110 @@ try:
|
|
| 26 |
app_private_key_string=os.environ.get("ALIPAY_PRIVATE_KEY", "").replace("\\n", "\n"),
|
| 27 |
alipay_public_key_string=os.environ.get("ALIPAY_PUBLIC_KEY", "").replace("\\n", "\n"),
|
| 28 |
sign_type="RSA2",
|
| 29 |
-
debug=False,
|
| 30 |
config=AliPayConfig(timeout=15)
|
| 31 |
)
|
| 32 |
except Exception as e:
|
| 33 |
alipay = None
|
| 34 |
-
print("⚠️ 支付宝 SDK 初始化跳过 (环境变量未配置,处于开发模式):", e)
|
| 35 |
-
|
| 36 |
|
| 37 |
def calculate_tx_hash(tx_id, account, tx_type, amount, prev_hash):
|
| 38 |
-
"""计算哈希值:依赖前一笔订单的哈希,实现链式防篡改"""
|
| 39 |
data = f"{tx_id}{account}{tx_type}{amount}{prev_hash}"
|
| 40 |
return hashlib.sha256(data.encode()).hexdigest()
|
| 41 |
|
| 42 |
def record_transaction(db: Session, account: str, tx_type: str, amount: int, target_id: str = None):
|
| 43 |
-
"""安全记录交易流水"""
|
| 44 |
last_tx = db.query(Transaction).filter(Transaction.account == account).order_by(Transaction.created_at.desc()).first()
|
| 45 |
prev_hash = last_tx.tx_hash if last_tx else "GENESIS"
|
| 46 |
-
|
| 47 |
tx_id = f"TX_{int(time.time())}_{uuid.uuid4().hex[:8]}"
|
| 48 |
tx_hash = calculate_tx_hash(tx_id, account, tx_type, amount, prev_hash)
|
| 49 |
-
|
| 50 |
new_tx = Transaction(tx_id=tx_id, account=account, tx_type=tx_type, amount=amount, target_id=target_id, prev_hash=prev_hash, tx_hash=tx_hash)
|
| 51 |
db.add(new_tx)
|
| 52 |
return new_tx
|
| 53 |
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
|
| 58 |
@router.post("/api/wallet/create_recharge_order")
|
| 59 |
async def create_recharge_order(req: RechargeRequest, db: Session = Depends(get_db)):
|
| 60 |
-
"
|
| 61 |
-
if not alipay:
|
| 62 |
-
raise HTTPException(status_code=500, detail="后台未配置支付密钥,暂不支持真实付款")
|
| 63 |
-
|
| 64 |
-
# 1. 使用 target_id 存储订单状态 "PENDING",不影响哈希校验
|
| 65 |
tx = record_transaction(db, req.account, "RECHARGE", req.amount, "PENDING")
|
| 66 |
out_trade_no = tx.tx_id
|
| 67 |
db.commit()
|
| 68 |
-
|
| 69 |
try:
|
| 70 |
-
|
| 71 |
-
result =
|
| 72 |
-
|
| 73 |
-
out_trade_no=out_trade_no,
|
| 74 |
-
total_amount=str(req.amount),
|
| 75 |
-
)
|
| 76 |
-
if result.get("code") == "10000":
|
| 77 |
-
return {"status": "success", "order_id": out_trade_no, "qr_code_url": result.get("qr_code")}
|
| 78 |
-
else:
|
| 79 |
-
raise HTTPException(status_code=500, detail=f"生成支付码失败: {result.get('msg')}")
|
| 80 |
except Exception as e:
|
| 81 |
raise HTTPException(status_code=500, detail=f"支付接口异常: {str(e)}")
|
| 82 |
|
| 83 |
@router.post("/api/wallet/alipay_notify")
|
| 84 |
async def alipay_notify(request: Request, db: Session = Depends(get_db)):
|
| 85 |
-
"""🚨 核心:接收支付宝付款成功的异步回调,验签并加钱"""
|
| 86 |
data = dict(await request.form())
|
| 87 |
signature = data.pop("sign", None)
|
| 88 |
-
|
| 89 |
-
if
|
| 90 |
-
return "fail"
|
| 91 |
-
|
| 92 |
-
# 1. 严格验签,防伪造
|
| 93 |
-
success = alipay.verify(data, signature)
|
| 94 |
-
if success and data.get("trade_status") in ("TRADE_SUCCESS", "TRADE_FINISHED"):
|
| 95 |
out_trade_no = data.get("out_trade_no")
|
| 96 |
total_amount = float(data.get("total_amount", 0))
|
| 97 |
-
|
| 98 |
-
# 2. 悲观锁查询该笔 PENDING 订单,防并发重复加款
|
| 99 |
tx = db.query(Transaction).filter(Transaction.tx_id == out_trade_no).with_for_update().first()
|
| 100 |
-
if not tx or tx.target_id == "SUCCESS":
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
if tx.amount != int(total_amount):
|
| 104 |
-
return "fail" # 金额对不上,拦截黑客攻击
|
| 105 |
-
|
| 106 |
-
# 3. 安全加款
|
| 107 |
wallet = db.query(Wallet).filter(Wallet.account == tx.account).with_for_update().first()
|
| 108 |
if not wallet:
|
| 109 |
wallet = Wallet(account=tx.account)
|
| 110 |
db.add(wallet)
|
| 111 |
-
|
| 112 |
wallet.balance += tx.amount
|
| 113 |
-
tx.target_id = "SUCCESS"
|
| 114 |
db.commit()
|
| 115 |
-
|
| 116 |
-
return "success" # 必须返回纯文本 success 给支付宝
|
| 117 |
return "fail"
|
| 118 |
|
| 119 |
@router.get("/api/wallet/check_order/{order_id}")
|
| 120 |
async def check_order(order_id: str, db: Session = Depends(get_db)):
|
| 121 |
-
"""前端轮询查单接口"""
|
| 122 |
tx = db.query(Transaction).filter(Transaction.tx_id == order_id).first()
|
| 123 |
-
if not tx:
|
| 124 |
-
|
| 125 |
-
return {"status": tx.target_id} # 返回 PENDING 或 SUCCESS
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
# =======================================================
|
| 129 |
-
# 🛒 消费闭环链路
|
| 130 |
-
# =======================================================
|
| 131 |
|
| 132 |
@router.post("/api/wallet/purchase")
|
| 133 |
async def purchase_item(req: PurchaseRequest, db: Session = Depends(get_db)):
|
| 134 |
-
"""消费购买接口:严密的并发防刷控制,以及早鸟保护机制"""
|
| 135 |
items_db = json_db.load_data("items.json", default_data=[])
|
| 136 |
item = next((i for i in items_db if i["id"] == req.item_id), None)
|
| 137 |
if not item: raise HTTPException(status_code=404, detail="商品不存在")
|
| 138 |
-
|
| 139 |
price = int(item.get("price", 0))
|
| 140 |
author = item.get("author")
|
| 141 |
-
|
| 142 |
owned = db.query(Ownership).filter(Ownership.account == req.account, Ownership.item_id == req.item_id).first()
|
| 143 |
-
if owned:
|
| 144 |
-
return {"status": "success", "message": "已永久授权,无需重复扣费", "already_owned": True}
|
| 145 |
-
|
| 146 |
if price <= 0 or req.account == author:
|
| 147 |
-
|
| 148 |
-
db.add(new_owner)
|
| 149 |
record_transaction(db, req.account, "CONSUME", 0, req.item_id)
|
| 150 |
db.commit()
|
| 151 |
-
return {"status": "success", "message": "免费
|
| 152 |
|
| 153 |
buyer_wallet = db.query(Wallet).filter(Wallet.account == req.account).with_for_update().first()
|
| 154 |
-
if not buyer_wallet or buyer_wallet.balance < price:
|
| 155 |
-
raise HTTPException(status_code=400, detail="余额不足")
|
| 156 |
-
|
| 157 |
buyer_wallet.balance -= price
|
| 158 |
record_transaction(db, req.account, "CONSUME", -price, req.item_id)
|
| 159 |
-
|
| 160 |
-
new_owner = Ownership(account=req.account, item_id=req.item_id)
|
| 161 |
-
db.add(new_owner)
|
| 162 |
|
| 163 |
author_wallet = db.query(Wallet).filter(Wallet.account == author).with_for_update().first()
|
| 164 |
if not author_wallet:
|
| 165 |
author_wallet = Wallet(account=author)
|
| 166 |
db.add(author_wallet)
|
| 167 |
-
|
| 168 |
author_wallet.earn_balance += price
|
| 169 |
record_transaction(db, author, "EARN", price, req.item_id)
|
| 170 |
-
|
| 171 |
db.commit()
|
| 172 |
return {"status": "success", "already_owned": False}
|
| 173 |
|
| 174 |
-
|
| 175 |
-
# =======================================================
|
| 176 |
-
# 💸 提现出金链路 (引入支付宝自动单笔打款)
|
| 177 |
-
# =======================================================
|
| 178 |
-
|
| 179 |
@router.post("/api/wallet/withdraw")
|
| 180 |
async def withdraw_earnings(req: WithdrawRequest, db: Session = Depends(get_db)):
|
| 181 |
-
"""提现接口"""
|
| 182 |
users_db = json_db.load_data("users.json", default_data={})
|
| 183 |
user = users_db.get(req.account)
|
| 184 |
if not user: raise HTTPException(status_code=404, detail="账号异常")
|
|
@@ -189,67 +136,59 @@ async def withdraw_earnings(req: WithdrawRequest, db: Session = Depends(get_db))
|
|
| 189 |
raise HTTPException(status_code=400, detail="验证码不正确或已过期")
|
| 190 |
|
| 191 |
wallet = db.query(Wallet).filter(Wallet.account == req.account).with_for_update().first()
|
| 192 |
-
if not wallet
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
if
|
| 199 |
-
|
|
|
|
|
|
|
| 200 |
wallet.earn_balance -= req.amount
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 201 |
wallet.frozen_balance += req.amount
|
| 202 |
record_transaction(db, req.account, "WITHDRAW_FREEZE", -req.amount, req.alipay_account)
|
| 203 |
VERIFY_CODES.pop(cache_key, None)
|
| 204 |
db.commit()
|
| 205 |
-
return {"status": "success", "earn_balance": wallet.earn_balance, "message": "提现申请已提交 (暂为开发模式
|
| 206 |
|
| 207 |
-
# 真实企业级模式:直接调用支付宝单笔转账接口打款到用户余额
|
| 208 |
out_biz_no = f"WD_{int(time.time())}_{uuid.uuid4().hex[:6]}"
|
| 209 |
try:
|
| 210 |
result = alipay.api_alipay_fund_trans_toaccount_transfer(
|
| 211 |
-
out_biz_no=out_biz_no,
|
| 212 |
-
|
| 213 |
-
payee_account=req.alipay_account,
|
| 214 |
-
amount=str(req.amount),
|
| 215 |
-
payee_real_name=req.real_name,
|
| 216 |
-
remark="ComfyUI社区精选创作者收益提现"
|
| 217 |
)
|
| 218 |
-
|
| 219 |
if result.get("code") == "10000":
|
| 220 |
-
# 打款秒成功,彻底扣除可提现收益
|
| 221 |
-
wallet.earn_balance -= req.amount
|
| 222 |
record_transaction(db, req.account, "WITHDRAW", -req.amount, req.alipay_account)
|
| 223 |
VERIFY_CODES.pop(cache_key, None)
|
| 224 |
db.commit()
|
| 225 |
-
return {"status": "success", "earn_balance": wallet.earn_balance, "message": "打款已秒到账
|
| 226 |
else:
|
| 227 |
raise HTTPException(status_code=400, detail=f"打款风控拦截: {result.get('sub_msg')}")
|
| 228 |
except Exception as e:
|
| 229 |
raise HTTPException(status_code=500, detail=f"提现接口异常: {str(e)}")
|
| 230 |
|
| 231 |
-
# 【修复 2】:去除了底部重复多余的旧代码,只保留最新正确的一个打赏接口
|
| 232 |
@router.post("/api/wallet/tip")
|
| 233 |
async def tip_user(req: TipRequest, db: Session = Depends(get_db)):
|
| 234 |
-
"
|
| 235 |
-
if req.
|
| 236 |
-
raise HTTPException(status_code=400, detail="不能打赏自己")
|
| 237 |
-
if req.amount <= 0:
|
| 238 |
-
raise HTTPException(status_code=400, detail="打赏金额必须大于0")
|
| 239 |
|
| 240 |
users_db = json_db.load_data("users.json", default_data={})
|
| 241 |
-
if req.target_account not in users_db:
|
| 242 |
-
raise HTTPException(status_code=404, detail="目标用户不存在")
|
| 243 |
-
|
| 244 |
target_user = users_db[req.target_account]
|
| 245 |
tips_received = target_user.get("tips_received", {})
|
| 246 |
|
| 247 |
-
# 计算等级额度 (100分=1星, 500分=1月, 2500分=1太阳。上限9太阳=22500分)
|
| 248 |
current_tip = tips_received.get(req.sender_account, {}).get("amount", 0)
|
| 249 |
if current_tip + req.amount > 22500:
|
| 250 |
raise HTTPException(status_code=400, detail=f"您对该用户的打赏已达上限 (9个太阳/22500积分),最多还能打赏 {22500 - current_tip} 积分")
|
| 251 |
|
| 252 |
-
# 1. 扣除打赏者余额 (悲观锁防并发)
|
| 253 |
sender_wallet = db.query(Wallet).filter(Wallet.account == req.sender_account).with_for_update().first()
|
| 254 |
if not sender_wallet or sender_wallet.balance < req.amount:
|
| 255 |
raise HTTPException(status_code=400, detail="积分余额不足,请先充值")
|
|
@@ -257,22 +196,25 @@ async def tip_user(req: TipRequest, db: Session = Depends(get_db)):
|
|
| 257 |
sender_wallet.balance -= req.amount
|
| 258 |
record_transaction(db, req.sender_account, "TIP_SEND", -req.amount, req.target_account)
|
| 259 |
|
| 260 |
-
# 2. 增加作者收益
|
| 261 |
target_wallet = db.query(Wallet).filter(Wallet.account == req.target_account).with_for_update().first()
|
| 262 |
if not target_wallet:
|
| 263 |
target_wallet = Wallet(account=req.target_account)
|
| 264 |
db.add(target_wallet)
|
| 265 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 266 |
record_transaction(db, req.target_account, "TIP_RECEIVE", req.amount, req.sender_account)
|
| 267 |
|
| 268 |
-
# 3. 永久记录到该用户的榜单 JSON 中
|
| 269 |
tips_received[req.sender_account] = {
|
| 270 |
"amount": current_tip + req.amount,
|
| 271 |
"is_anonymous": req.is_anonymous,
|
| 272 |
"sender_name": users_db.get(req.sender_account, {}).get("name", req.sender_account)
|
| 273 |
}
|
| 274 |
target_user["tips_received"] = tips_received
|
| 275 |
-
|
| 276 |
db.commit()
|
| 277 |
json_db.save_data("users.json", users_db)
|
| 278 |
|
|
|
|
| 7 |
import os
|
| 8 |
from database_sql import get_db
|
| 9 |
from models_sql import Wallet, Transaction, Ownership
|
|
|
|
| 10 |
from models import RechargeRequest, WithdrawRequest, PurchaseRequest, TipRequest
|
| 11 |
import 数据库连接 as json_db
|
| 12 |
from router_users import VERIFY_CODES
|
| 13 |
|
| 14 |
router = APIRouter()
|
| 15 |
|
|
|
|
|
|
|
|
|
|
| 16 |
try:
|
| 17 |
from alipay import AliPay
|
| 18 |
from alipay.utils import AliPayConfig
|
|
|
|
| 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 |
def record_transaction(db: Session, account: str, tx_type: str, amount: int, target_id: str = None):
|
|
|
|
| 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 "GENESIS"
|
|
|
|
| 38 |
tx_id = f"TX_{int(time.time())}_{uuid.uuid4().hex[:8]}"
|
| 39 |
tx_hash = calculate_tx_hash(tx_id, account, tx_type, amount, prev_hash)
|
|
|
|
| 40 |
new_tx = Transaction(tx_id=tx_id, account=account, tx_type=tx_type, amount=amount, target_id=target_id, prev_hash=prev_hash, tx_hash=tx_hash)
|
| 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.earn_balance,
|
| 53 |
+
"tip_balance": getattr(wallet, 'tip_balance', 0),
|
| 54 |
+
"frozen_balance": wallet.frozen_balance
|
| 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="账号异常")
|
|
|
|
| 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: raise HTTPException(status_code=400, detail="不能打赏自己")
|
| 181 |
+
if req.amount <= 0: raise HTTPException(status_code=400, detail="打赏金额必须大于0")
|
|
|
|
|
|
|
|
|
|
| 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="积分余额不足,请先充值")
|
|
|
|
| 196 |
sender_wallet.balance -= req.amount
|
| 197 |
record_transaction(db, req.sender_account, "TIP_SEND", -req.amount, req.target_account)
|
| 198 |
|
|
|
|
| 199 |
target_wallet = db.query(Wallet).filter(Wallet.account == req.target_account).with_for_update().first()
|
| 200 |
if not target_wallet:
|
| 201 |
target_wallet = Wallet(account=req.target_account)
|
| 202 |
db.add(target_wallet)
|
| 203 |
+
|
| 204 |
+
# 【核心修改】:打赏金额独立进入 tip_balance 账目
|
| 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 |
tips_received[req.sender_account] = {
|
| 213 |
"amount": current_tip + req.amount,
|
| 214 |
"is_anonymous": req.is_anonymous,
|
| 215 |
"sender_name": users_db.get(req.sender_account, {}).get("name", req.sender_account)
|
| 216 |
}
|
| 217 |
target_user["tips_received"] = tips_received
|
|
|
|
| 218 |
db.commit()
|
| 219 |
json_db.save_data("users.json", users_db)
|
| 220 |
|