133 lines
4.7 KiB
Python
133 lines
4.7 KiB
Python
import json
|
|
from decimal import Decimal, ROUND_HALF_UP
|
|
from uuid import uuid4
|
|
|
|
from fastapi import APIRouter, HTTPException, Request, status
|
|
|
|
from app.config import wechat_pay_settings
|
|
from app.database import Database
|
|
from app.schemas import WeChatJSAPIPayRequest, WeChatJSAPIPayResponse
|
|
from app.temple_scope import TempleId, ensure_temple_exists
|
|
from app.wechat_pay import (
|
|
build_jsapi_pay_params,
|
|
decrypt_notify_resource,
|
|
post_wechat_json,
|
|
verify_notify_signature,
|
|
)
|
|
|
|
|
|
router = APIRouter(prefix="/payments", tags=["payments"])
|
|
|
|
|
|
def yuan_to_fen(amount: float) -> int:
|
|
return int((Decimal(str(amount)) * Decimal("100")).quantize(Decimal("1"), rounding=ROUND_HALF_UP))
|
|
|
|
|
|
def get_order_row(db: Database, temple_id: int, order_id: int):
|
|
row = db.execute(
|
|
"""
|
|
SELECT id, temple_id, item_name, amount, status
|
|
FROM orders
|
|
WHERE id = ? AND temple_id = ?
|
|
""",
|
|
(order_id, temple_id),
|
|
).fetchone()
|
|
if row is None:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="订单不存在")
|
|
return row
|
|
|
|
|
|
@router.post("/wechat/jsapi", response_model=WeChatJSAPIPayResponse)
|
|
def create_wechat_jsapi_payment(payload: WeChatJSAPIPayRequest, db: Database, temple_id: TempleId) -> WeChatJSAPIPayResponse:
|
|
ensure_temple_exists(db, temple_id)
|
|
order = get_order_row(db, temple_id, payload.order_id)
|
|
if order["status"] == "paid":
|
|
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="订单已支付,无需重复发起支付")
|
|
|
|
amount = yuan_to_fen(float(order["amount"]))
|
|
if amount <= 0:
|
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="订单金额必须大于 0")
|
|
|
|
out_trade_no = f"T{temple_id}O{order['id']}{uuid4().hex[:16]}"
|
|
description = (payload.description or order["item_name"])[:127]
|
|
wechat_payload = {
|
|
"appid": wechat_pay_settings.appid,
|
|
"mchid": wechat_pay_settings.mch_id,
|
|
"description": description,
|
|
"out_trade_no": out_trade_no,
|
|
"notify_url": wechat_pay_settings.notify_url,
|
|
"amount": {
|
|
"total": amount,
|
|
"currency": "CNY",
|
|
},
|
|
"payer": {
|
|
"openid": payload.openid,
|
|
},
|
|
}
|
|
result = post_wechat_json("/v3/pay/transactions/jsapi", wechat_payload)
|
|
prepay_id = result.get("prepay_id")
|
|
if not prepay_id:
|
|
raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail="微信支付未返回 prepay_id")
|
|
|
|
db.execute(
|
|
"""
|
|
INSERT INTO payment_transactions
|
|
(temple_id, order_id, out_trade_no, amount, status, prepay_id)
|
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
""",
|
|
(temple_id, order["id"], out_trade_no, amount, "prepay", prepay_id),
|
|
)
|
|
db.commit()
|
|
|
|
return WeChatJSAPIPayResponse(
|
|
**build_jsapi_pay_params(prepay_id),
|
|
out_trade_no=out_trade_no,
|
|
prepay_id=prepay_id,
|
|
)
|
|
|
|
|
|
@router.post("/wechat/notify")
|
|
async def handle_wechat_pay_notify(request: Request, db: Database) -> dict[str, str]:
|
|
body = (await request.body()).decode("utf-8")
|
|
headers = {key.lower(): value for key, value in request.headers.items()}
|
|
if not verify_notify_signature(headers, body):
|
|
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="微信支付通知验签失败")
|
|
|
|
notify_payload = json.loads(body)
|
|
resource = notify_payload.get("resource")
|
|
if not isinstance(resource, dict):
|
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="微信支付通知缺少 resource")
|
|
|
|
trade = decrypt_notify_resource(resource)
|
|
out_trade_no = trade.get("out_trade_no")
|
|
trade_state = trade.get("trade_state")
|
|
transaction_id = trade.get("transaction_id")
|
|
if not out_trade_no:
|
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="微信支付通知缺少商户订单号")
|
|
|
|
payment = db.execute(
|
|
"""
|
|
SELECT id, order_id
|
|
FROM payment_transactions
|
|
WHERE out_trade_no = ?
|
|
""",
|
|
(out_trade_no,),
|
|
).fetchone()
|
|
if payment is None:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="支付流水不存在")
|
|
|
|
status_value = "paid" if trade_state == "SUCCESS" else str(trade_state or "unknown").lower()
|
|
db.execute(
|
|
"""
|
|
UPDATE payment_transactions
|
|
SET status = ?, transaction_id = ?, raw_notify_json = ?, updated_at = CURRENT_TIMESTAMP
|
|
WHERE id = ?
|
|
""",
|
|
(status_value, transaction_id, json.dumps(trade, ensure_ascii=False), payment["id"]),
|
|
)
|
|
if trade_state == "SUCCESS":
|
|
db.execute("UPDATE orders SET status = 'paid' WHERE id = ?", (payment["order_id"],))
|
|
db.commit()
|
|
|
|
return {"code": "SUCCESS", "message": "成功"}
|