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": "成功"}