import hashlib import hmac import json import os import uuid from datetime import datetime from decimal import Decimal, InvalidOperation, ROUND_HALF_UP from urllib.parse import parse_qs from fastapi import APIRouter, Body, Depends, Header, HTTPException, Query, Request, Response from sqlalchemy import func from sqlalchemy.orm import Session from database import get_db from models import ( InvoiceApplication, PaymentRefund, PaymentTransaction, TokenAccount, TokenPaymentOrder, TokenPlan, TokenUsage, User, ) from responses import fail, ok from services.huihui_payment import HuihuiPaymentClient, HuihuiPaymentError from services.token_billing import get_or_create_account from services.wechat_virtual_payment import ( PAYMENT_EVENTS as WECHAT_PAYMENT_EVENTS, REFUND_EVENTS as WECHAT_REFUND_EVENTS, WechatVirtualPaymentError, build_payment_params as build_wechat_virtual_payment_params, callback_value as wechat_callback_value, exchange_code as exchange_wechat_code, parse_callback_body as parse_wechat_callback_body, product_id_for_plan, query_order as query_wechat_virtual_order, request_refund as request_wechat_virtual_refund, verify_callback_signature as verify_wechat_callback_signature, virtual_env as wechat_virtual_env, ) router = APIRouter(tags=["Token"]) PAYMENT_METHODS = {"wechat": "WECHAT", "alipay": "ALIPAY"} PAYMENT_SCENES = {"APP", "H5", "LITE", "JSAPI"} SUCCESS_STATUSES = {"SUCCESS", "SUCCEEDED", "PAID", "COMPLETED", "TRADE_SUCCESS"} FAILED_STATUSES = {"FAIL", "FAILED", "CLOSED", "CANCELLED", "CANCELED", "EXPIRED"} def _require_user(authorization: str | None, db: Session) -> User: if not authorization: raise HTTPException(status_code=401, detail="未登录") token = authorization.replace("Bearer ", "", 1).replace("bearer ", "", 1).strip() user = db.query(User).filter(User.app_token == token).first() if not user: raise HTTPException(status_code=401, detail="会话无效或已过期") return user def _require_finance_admin(value: str | None): expected = os.getenv("AVATAR_FINANCE_ADMIN_SECRET", "").strip() provided = str(value or "").strip() if len(expected) < 16 or not hmac.compare_digest(provided, expected): raise HTTPException(status_code=403, detail="财务管理凭证无效") def _payment_client() -> HuihuiPaymentClient: return HuihuiPaymentClient({ "HUIHUI_PAYMENT_BASE_URL": os.getenv( "HUIHUI_PAYMENT_BASE_URL", "https://open.99hui.com/api/payment-v3" ), "HUIHUI_APP_ID": os.getenv("HUIHUI_APP_ID", ""), "HUIHUI_ACCESS_ID": os.getenv("HUIHUI_ACCESS_ID", ""), "HUIHUI_ACCESS_SECRET": os.getenv("HUIHUI_ACCESS_SECRET", ""), "HUIHUI_PAYMENT_TIMEOUT_SECONDS": os.getenv("HUIHUI_PAYMENT_TIMEOUT_SECONDS", "30"), }) def _callback_url(order_no: str) -> str: base = os.getenv( "HUIHUI_PAYMENT_CALLBACK_BASE_URL", "https://digital.99hui.com" ).rstrip("/") secret = os.getenv("HUIHUI_PAYMENT_CALLBACK_SECRET", "").strip() if len(secret) < 16: raise HuihuiPaymentError("会会支付回调密钥未配置") signature = hmac.new(secret.encode(), order_no.encode(), hashlib.sha256).hexdigest() return f"{base}/api/token/payment/callback/{order_no}/{signature}" def _price_cents(price: float) -> int: return int( (Decimal(str(price)) * Decimal("100")).quantize( Decimal("1"), rounding=ROUND_HALF_UP ) ) def _payment_payload(order: TokenPaymentOrder, account: TokenAccount) -> dict: return {**order.to_dict(), "balance": account.balance} def _safe_event_summary(payload: dict) -> str: """Persist only reconciliation fields, never signatures, tokens or session keys.""" summary = {} for key in ( "Event", "OutTradeNo", "OpenId", "Env", "MchOrderId", "MchRefundId", "WxRefundId", "RefundFee", "RetCode", "RetMsg", ): value = wechat_callback_value(payload, key) if value not in (None, ""): summary[key] = value goods = wechat_callback_value(payload, "GoodsInfo") if isinstance(goods, dict): summary["GoodsInfo"] = { key: goods.get(key) for key in ("ProductId", "Quantity", "OrigPrice", "ActualPrice") if goods.get(key) not in (None, "") } return json.dumps(summary, ensure_ascii=False, separators=(",", ":"))[:2000] def _record_transaction( db: Session, *, order: TokenPaymentOrder, provider: str, status: str, amount_cents: int, event_type: str = "payment", transaction_no: str = "", raw_summary: str = "", ): if transaction_no: duplicate = db.query(PaymentTransaction).filter( PaymentTransaction.provider == provider, PaymentTransaction.transaction_no == transaction_no, PaymentTransaction.event_type == event_type, ).first() if duplicate: return duplicate row = PaymentTransaction( order_no=order.order_no, provider=provider, transaction_no=transaction_no, event_type=event_type, status=status, amount_cents=amount_cents, raw_summary=raw_summary, ) db.add(row) return row def _settle_paid_order( db: Session, order: TokenPaymentOrder, *, provider_status: str, transaction_no: str = "", raw_summary: str = "", ) -> bool: if order.status in {"paid", "refunded"}: return False updated = db.query(TokenPaymentOrder).filter( TokenPaymentOrder.id == order.id, TokenPaymentOrder.status.in_(["pending", "failed", "closed"]), ).update({ TokenPaymentOrder.status: "paid", TokenPaymentOrder.provider_status: provider_status, TokenPaymentOrder.paid_at: datetime.utcnow(), TokenPaymentOrder.failure_reason: "", }, synchronize_session=False) if not updated: return False account = get_or_create_account(db, order.user_id) account.balance = int(account.balance or 0) + order.points_amount account.total_granted = int(account.total_granted or 0) + order.points_amount _record_transaction( db, order=order, provider=order.provider, status="paid", amount_cents=order.price_cents, transaction_no=transaction_no, raw_summary=raw_summary, ) return True def _complete_refund( db: Session, order: TokenPaymentOrder, refund: PaymentRefund, *, provider_refund_no: str = "", failure_reason: str = "", ): if failure_reason: refund.status = "failed" refund.failure_reason = failure_reason[:500] order.refund_status = "failed" return if refund.status == "succeeded": return account = get_or_create_account(db, order.user_id) # Provider-confirmed refunds must claw back the full grant. A negative # balance records consumed refunded points and blocks further usage. account.balance = int(account.balance or 0) - int(refund.points_amount or 0) account.total_granted = max(0, int(account.total_granted or 0) - int(refund.points_amount or 0)) refund.status = "succeeded" refund.provider_refund_no = provider_refund_no[:128] refund.failure_reason = "" refund.completed_at = datetime.utcnow() order.status = "refunded" order.refund_status = "succeeded" order.refunded_at = datetime.utcnow() _record_transaction( db, order=order, provider=order.provider, status="succeeded", amount_cents=refund.amount_cents, event_type="refund", transaction_no=provider_refund_no or refund.refund_no, ) def _nested_payload(value): if isinstance(value, str): text = value.strip() if text[:1] in ("{", "["): try: return _nested_payload(json.loads(text)) except (TypeError, ValueError): return value return value if isinstance(value, list): return [_nested_payload(item) for item in value] if isinstance(value, dict): return {key: _nested_payload(item) for key, item in value.items()} return value def _find_value(payload, *names): expected = {name.lower() for name in names} if isinstance(payload, dict): for key, value in payload.items(): if key.lower() in expected and value not in (None, ""): return value for value in payload.values(): found = _find_value(value, *names) if found not in (None, ""): return found elif isinstance(payload, list): for value in payload: found = _find_value(value, *names) if found not in (None, ""): return found return None def _callback_amount_cents(payload) -> int | None: value = _find_value( payload, "actualAmt", "payAmt", "masterOrderAmt", "orderAmt", "amount", "totalAmount", ) if value in (None, ""): return None try: return int( (Decimal(str(value)) * Decimal("100")).quantize( Decimal("1"), rounding=ROUND_HALF_UP ) ) except (InvalidOperation, TypeError, ValueError): return None @router.get("/token/balance") def balance(authorization: str = Header(None), db: Session = Depends(get_db)): user = _require_user(authorization, db) acc = get_or_create_account(db, user.id) return ok({ "balance": acc.balance, "totalGranted": acc.total_granted, "totalConsumed": acc.total_consumed, }) @router.get("/token/plans") def plans(authorization: str = Header(None), db: Session = Depends(get_db)): _require_user(authorization, db) items = db.query(TokenPlan).order_by(TokenPlan.price.asc()).all() return ok([p.to_dict() for p in items]) # 积分只会在会会支付回调确认成功后到账。 @router.post("/token/charge") def charge(payload: dict = Body(...), authorization: str = Header(None), db: Session = Depends(get_db)): user = _require_user(authorization, db) plan = db.query(TokenPlan).filter(TokenPlan.id == payload.get("planId")).first() if not plan: return fail("套餐不存在", 404) payment_method = str(payload.get("paymentMethod") or "").lower() pay_type = PAYMENT_METHODS.get(payment_method) if not pay_type: return fail("请选择正确的支付方式", 400) pay_way = str(payload.get("payScene") or "APP").upper() if pay_way not in PAYMENT_SCENES: return fail("当前支付场景不受支持", 400) if pay_way == "LITE" and payment_method != "wechat": return fail("微信小程序虚拟支付仅支持微信支付", 400) cents = _price_cents(plan.price) provider = "wechat_virtual" if pay_way == "LITE" else "huihui" order = TokenPaymentOrder( order_no=f"AV{datetime.utcnow().strftime('%Y%m%d%H%M%S')}{uuid.uuid4().hex[:12].upper()}", user_id=user.id, plan_id=plan.id, payment_method=payment_method, pay_type=pay_type, pay_way=pay_way, points_amount=plan.amount, price_cents=cents, status="pending", provider=provider, ) db.add(order) db.commit() if provider == "wechat_virtual": if not user.wechat_mp_openid or not user.wechat_mp_session_key: order.status = "failed" order.failure_reason = "微信小程序登录态尚未准备好,请重新进入支付页" db.commit() return fail(order.failure_reason, 409) try: result = build_wechat_virtual_payment_params( order=order, plan=plan, session_key=user.wechat_mp_session_key, ) except WechatVirtualPaymentError as exc: order.status = "failed" order.failure_reason = str(exc)[:500] db.commit() return fail(str(exc), 503) order.provider_order_id = order.order_no order.provider_order_no = order.order_no order.provider_status = "CREATED" order.pay_message = json.dumps(result, ensure_ascii=False, separators=(",", ":")) db.commit() return ok(_payment_payload(order, get_or_create_account(db, user.id))) try: callback_url = _callback_url(order.order_no) except HuihuiPaymentError as exc: order.status = "failed" order.failure_reason = str(exc) db.commit() return fail(str(exc), 503) try: result = _payment_client().create_payment( huihui_token=user.huihui_token, huihui_user_id=user.huihui_user_id, real_name=user.nickname, order_no=order.order_no, amount=f"{cents / 100:.2f}", points_amount=plan.amount, pay_type=pay_type, pay_way=pay_way, callback_url=callback_url, ) except HuihuiPaymentError as exc: order.status = "failed" order.failure_reason = str(exc)[:500] db.commit() return fail(str(exc), 502) db.refresh(order) if order.status != "paid": order.provider_order_id = str(result.get("orderId") or "") order.provider_order_no = str(result.get("orderNo") or "") order.provider_status = str(result.get("status") or "pending") message = result.get("payMessage") or "" order.pay_message = ( json.dumps(message, ensure_ascii=False) if isinstance(message, (dict, list)) else str(message) ) if order.provider_status.upper() in FAILED_STATUSES: order.status = "failed" order.failure_reason = str(result.get("bankReturnMsg") or "支付下单失败")[:500] db.commit() return ok(_payment_payload(order, get_or_create_account(db, user.id))) @router.get("/token/payment/{order_id}") def payment_status(order_id: str, authorization: str = Header(None), db: Session = Depends(get_db)): user = _require_user(authorization, db) order = db.query(TokenPaymentOrder).filter( TokenPaymentOrder.id == order_id, TokenPaymentOrder.user_id == user.id, ).first() if not order: return fail("支付订单不存在", 404) if order.provider == "wechat_virtual" and order.status == "pending" and user.wechat_mp_openid: try: provider_data = query_wechat_virtual_order( openid=user.wechat_mp_openid, order_no=order.order_no, ) provider_order = provider_data.get("order") or {} provider_status = int(provider_order.get("status", 0) or 0) paid_cents = int(provider_order.get("paid_fee") or provider_order.get("order_fee") or 0) order.provider_status = str(provider_status) if provider_status in {2, 3, 4} and paid_cents == order.price_cents: _settle_paid_order( db, order, provider_status=f"XPAY_{provider_status}", transaction_no=str( provider_order.get("wxpay_order_id") or provider_order.get("channel_order_id") or order.order_no ), ) elif provider_status == 6: order.status = "failed" order.failure_reason = "微信虚拟支付订单已关闭" db.commit() db.refresh(order) except WechatVirtualPaymentError: # 回调仍是首选确认路径;短暂查询失败不覆盖订单状态。 pass return ok(_payment_payload(order, get_or_create_account(db, user.id))) @router.post("/token/wechat/session") def bind_wechat_session( payload: dict = Body(...), authorization: str = Header(None), db: Session = Depends(get_db), ): user = _require_user(authorization, db) code = str(payload.get("code") or "").strip() if not code or len(code) > 256: return fail("微信登录凭证无效", 400) try: session = exchange_wechat_code(code) except WechatVirtualPaymentError as exc: return fail(str(exc), 502) conflict = db.query(User).filter( User.wechat_mp_openid == session["openid"], User.id != user.id, ).first() if conflict: return fail("该微信账号已绑定其他会会账号", 409) user.wechat_mp_openid = session["openid"] user.wechat_mp_session_key = session["session_key"] db.commit() return ok({"ready": True}) @router.get("/token/orders") def list_user_orders( page: int = Query(1, ge=1), page_size: int = Query(20, ge=1, le=100), authorization: str = Header(None), db: Session = Depends(get_db), ): user = _require_user(authorization, db) query = db.query(TokenPaymentOrder).filter(TokenPaymentOrder.user_id == user.id) total = query.count() orders = query.order_by(TokenPaymentOrder.created_at.desc()).offset((page - 1) * page_size).limit(page_size).all() invoice_by_order = { item.order_no: item.to_dict() for item in db.query(InvoiceApplication).filter( InvoiceApplication.order_no.in_([order.order_no for order in orders]) ).all() } if orders else {} return ok({ "total": total, "page": page, "pageSize": page_size, "items": [ {**_payment_payload(order, get_or_create_account(db, user.id)), "invoice": invoice_by_order.get(order.order_no)} for order in orders ], }) @router.post("/token/orders/{order_no}/invoice") def apply_invoice( order_no: str, payload: dict = Body(...), authorization: str = Header(None), db: Session = Depends(get_db), ): user = _require_user(authorization, db) order = db.query(TokenPaymentOrder).filter( TokenPaymentOrder.order_no == order_no, TokenPaymentOrder.user_id == user.id, ).first() if not order: return fail("订单不存在", 404) if order.status != "paid" or order.refund_status not in {"", "none"}: return fail("只有已支付且未退款的订单可以申请发票", 409) title = str(payload.get("title") or "").strip() invoice_type = str(payload.get("invoiceType") or "personal").strip().lower() tax_number = str(payload.get("taxNumber") or "").strip().upper() email = str(payload.get("email") or "").strip() if not title or len(title) > 120: return fail("请填写正确的发票抬头", 400) if invoice_type not in {"personal", "company"}: return fail("发票类型不正确", 400) if invoice_type == "company" and (len(tax_number) < 15 or len(tax_number) > 20): return fail("请填写正确的企业税号", 400) if email and ("@" not in email or len(email) > 160): return fail("请填写正确的接收邮箱", 400) invoice = db.query(InvoiceApplication).filter(InvoiceApplication.order_no == order_no).first() if invoice and invoice.status not in {"rejected", "cancelled"}: return fail("该订单已申请发票", 409) if invoice is None: invoice = InvoiceApplication(order_no=order_no, user_id=user.id, amount_cents=order.price_cents) db.add(invoice) invoice.title = title invoice.invoice_type = invoice_type invoice.tax_number = tax_number if invoice_type == "company" else "" invoice.email = email invoice.status = "pending" invoice.remark = "" db.commit() db.refresh(invoice) return ok(invoice.to_dict()) @router.post("/token/admin/orders/{order_no}/refund") def admin_request_refund( order_no: str, payload: dict = Body(...), finance_key: str = Header(None, alias="X-Avatar-Finance-Key"), db: Session = Depends(get_db), ): _require_finance_admin(finance_key) order = db.query(TokenPaymentOrder).filter(TokenPaymentOrder.order_no == order_no).first() if not order: return fail("订单不存在", 404) if order.status != "paid" or order.refund_status not in {"", "none", "failed"}: return fail("该订单当前不可退款", 409) account = get_or_create_account(db, order.user_id) if int(account.balance or 0) < int(order.points_amount or 0): return fail("该订单发放的积分已使用,不能执行全额退款", 409) invoice = db.query(InvoiceApplication).filter(InvoiceApplication.order_no == order.order_no).first() if invoice and invoice.status == "issued": return fail("该订单发票已开具,请先完成红冲再退款", 409) reason = str(payload.get("reason") or "后台退款").strip() if not reason or len(reason) > 200: return fail("请填写 200 字以内的退款原因", 400) refund = PaymentRefund( refund_no=f"RF{datetime.utcnow().strftime('%Y%m%d%H%M%S')}{uuid.uuid4().hex[:10].upper()}", order_no=order.order_no, amount_cents=order.price_cents, points_amount=order.points_amount, reason=reason, status="processing", requested_by=str(payload.get("operator") or "admin")[:80], ) claimed = db.query(TokenPaymentOrder).filter( TokenPaymentOrder.id == order.id, TokenPaymentOrder.status == "paid", TokenPaymentOrder.refund_status.in_(["", "none", "failed"]), ).update({TokenPaymentOrder.refund_status: "processing"}, synchronize_session=False) if not claimed: db.rollback() return fail("该订单已有退款任务正在处理", 409) db.add(refund) if invoice and invoice.status == "pending": invoice.status = "cancelled" invoice.remark = "订单已申请退款,发票申请自动取消" db.commit() user = db.query(User).filter(User.id == order.user_id).first() try: if order.provider == "wechat_virtual": if not user or not user.wechat_mp_openid: raise WechatVirtualPaymentError("订单缺少微信 OpenID,无法退款") provider_result = request_wechat_virtual_refund( openid=user.wechat_mp_openid, order_no=order.order_no, refund_no=refund.refund_no, amount_cents=refund.amount_cents, ) else: provider_result = _payment_client().request_refund( huihui_token=user.huihui_token if user else "", huihui_user_id=user.huihui_user_id if user else "", order_no=order.order_no, refund_no=refund.refund_no, amount=f"{refund.amount_cents / 100:.2f}", reason=reason, ) except (WechatVirtualPaymentError, HuihuiPaymentError) as exc: _complete_refund(db, order, refund, failure_reason=str(exc)) db.commit() return fail(str(exc), 502) provider_status = str( provider_result.get("status") or provider_result.get("refundStatus") or provider_result.get("result") or "PROCESSING" ).upper() provider_refund_no = str( provider_result.get("refundNo") or provider_result.get("refundId") or provider_result.get("wx_refund_id") or "" ) refund.provider_refund_no = provider_refund_no[:128] if provider_status in {"SUCCESS", "SUCCEEDED", "REFUNDED", "COMPLETED"}: _complete_refund(db, order, refund, provider_refund_no=provider_refund_no) db.commit() db.refresh(refund) return ok(refund.to_dict()) @router.post("/token/admin/refunds/{refund_no}/confirm") def admin_confirm_refund( refund_no: str, payload: dict = Body(...), finance_key: str = Header(None, alias="X-Avatar-Finance-Key"), db: Session = Depends(get_db), ): """Record a provider-console reconciliation result for asynchronous refunds.""" _require_finance_admin(finance_key) refund = db.query(PaymentRefund).filter(PaymentRefund.refund_no == refund_no).first() if not refund: return fail("退款单不存在", 404) order = db.query(TokenPaymentOrder).filter(TokenPaymentOrder.order_no == refund.order_no).first() if not order: return fail("原支付订单不存在", 404) status = str(payload.get("status") or "").lower() if status == "succeeded": _complete_refund( db, order, refund, provider_refund_no=str(payload.get("providerRefundNo") or refund.provider_refund_no or ""), ) elif status == "failed": _complete_refund( db, order, refund, failure_reason=str(payload.get("failureReason") or "供应商退款失败"), ) else: return fail("退款确认状态只能是 succeeded 或 failed", 400) db.commit() db.refresh(refund) return ok(refund.to_dict()) @router.post("/token/payment/callback/{order_no}/{callback_signature}") async def payment_callback( order_no: str, callback_signature: str, request: Request, db: Session = Depends(get_db), ): secret = os.getenv("HUIHUI_PAYMENT_CALLBACK_SECRET", "").strip() expected = hmac.new(secret.encode(), order_no.encode(), hashlib.sha256).hexdigest() if len(secret) < 16 or not hmac.compare_digest(callback_signature, expected): raise HTTPException(status_code=404, detail="Not found") content_type = request.headers.get("content-type", "").lower() if "application/json" in content_type: try: payload = await request.json() except ValueError: return fail("支付回调格式不正确", 400) else: raw = (await request.body()).decode("utf-8", errors="replace") payload = {key: values[-1] for key, values in parse_qs(raw).items()} payload = _nested_payload(payload) payload_order_no = str(_find_value( payload, "masterOrderNo", "master_order_no", "orderNo", "order_no", "bizOrderNo", ) or "").strip() if payload_order_no and payload_order_no != order_no: return fail("支付回调订单号不匹配", 422) order = db.query(TokenPaymentOrder).filter(TokenPaymentOrder.order_no == order_no).first() if not order: return fail("支付订单不存在", 404) if order.status == "paid": return ok({"received": True, "duplicate": True}) if order.status == "refunded": return ok({"received": True, "duplicate": True, "refunded": True}) provider_status = str(_find_value( payload, "status", "payStatus", "tradeStatus", "paymentStatus" ) or "").upper() order.provider_status = provider_status if provider_status not in SUCCESS_STATUSES: if provider_status in FAILED_STATUSES: order.status = "failed" order.failure_reason = str( _find_value(payload, "message", "errorMsg", "failReason") or "支付失败" )[:500] _record_transaction( db, order=order, provider="huihui", status="failed", amount_cents=order.price_cents, transaction_no=str(_find_value(payload, "transactionId", "tradeNo") or ""), ) db.commit() return ok({"received": True, "paid": False}) paid_cents = _callback_amount_cents(payload) if paid_cents is None or paid_cents != order.price_cents: order.failure_reason = "支付回调金额不匹配" db.commit() return fail("支付金额不匹配", 422) _settle_paid_order( db, order, provider_status=provider_status, transaction_no=str(_find_value(payload, "transactionId", "tradeNo", "paymentNo") or ""), ) db.commit() return ok({"received": True, "paid": True}) def _wechat_notify_response(request: Request, *, success: bool, message: str = ""): code = 0 if success else 1 text = "success" if success else (message or "fail")[:200].replace("]]>", "") if "xml" in (request.headers.get("content-type") or "").lower(): return Response( content=f"{code}", media_type="application/xml", ) return {"ErrCode": code, "ErrMsg": text} @router.get("/token/payment/wechat/virtual/notify") def validate_wechat_virtual_notify( signature: str = Query(""), timestamp: str = Query(""), nonce: str = Query(""), echostr: str = Query(""), ): if not verify_wechat_callback_signature(signature, timestamp, nonce): raise HTTPException(status_code=403, detail="invalid signature") return Response(content=echostr or "ok", media_type="text/plain") @router.post("/token/payment/wechat/virtual/notify") async def wechat_virtual_notify( request: Request, signature: str = Query(""), timestamp: str = Query(""), nonce: str = Query(""), db: Session = Depends(get_db), ): if not verify_wechat_callback_signature(signature, timestamp, nonce): return _wechat_notify_response(request, success=False, message="invalid signature") try: payload = _nested_payload(parse_wechat_callback_body(await request.body())) except WechatVirtualPaymentError as exc: return _wechat_notify_response(request, success=False, message=str(exc)) event = str(wechat_callback_value(payload, "Event") or "").lower() if event in WECHAT_PAYMENT_EVENTS: order_no = str(wechat_callback_value(payload, "OutTradeNo") or "").strip() order = db.query(TokenPaymentOrder).filter(TokenPaymentOrder.order_no == order_no).first() if not order or order.provider != "wechat_virtual": return _wechat_notify_response(request, success=False, message="order not found") user = db.query(User).filter(User.id == order.user_id).first() openid = str(wechat_callback_value(payload, "OpenId") or "").strip() if not user or not openid or openid != user.wechat_mp_openid: return _wechat_notify_response(request, success=False, message="openid mismatch") try: callback_env = int(wechat_callback_value(payload, "Env")) actual_price = int(wechat_callback_value(payload, "GoodsInfo", "ActualPrice")) except (TypeError, ValueError): return _wechat_notify_response(request, success=False, message="invalid payment amount") plan = db.query(TokenPlan).filter(TokenPlan.id == order.plan_id).first() product_id = str(wechat_callback_value(payload, "GoodsInfo", "ProductId") or "") try: expected_product_id = product_id_for_plan(plan) if plan else "" except WechatVirtualPaymentError: expected_product_id = "" if ( callback_env != wechat_virtual_env() or actual_price != order.price_cents or not expected_product_id or product_id != expected_product_id ): return _wechat_notify_response(request, success=False, message="payment verification failed") transaction_no = str( wechat_callback_value(payload, "WeChatPayInfo", "TransactionId") or wechat_callback_value(payload, "WeChatPayInfo", "MchOrderNo") or order_no ) _settle_paid_order( db, order, provider_status=event, transaction_no=transaction_no, raw_summary=_safe_event_summary(payload), ) db.commit() return _wechat_notify_response(request, success=True) if event in WECHAT_REFUND_EVENTS: order_no = str(wechat_callback_value(payload, "MchOrderId") or "").strip() refund_no = str(wechat_callback_value(payload, "MchRefundId") or "").strip() order = db.query(TokenPaymentOrder).filter(TokenPaymentOrder.order_no == order_no).first() if not order or order.provider != "wechat_virtual": return _wechat_notify_response(request, success=False, message="order not found") if order.status == "refunded" or order.refund_status == "succeeded": return _wechat_notify_response(request, success=True) try: refund_cents = int(wechat_callback_value(payload, "RefundFee") or 0) result_code_value = wechat_callback_value(payload, "RetCode") if result_code_value in (None, ""): raise ValueError("missing RetCode") result_code = int(result_code_value) except (TypeError, ValueError): return _wechat_notify_response(request, success=False, message="invalid refund") refund = db.query(PaymentRefund).filter(PaymentRefund.refund_no == refund_no).first() if refund is None: refund = PaymentRefund( refund_no=refund_no or f"WR{uuid.uuid4().hex[:20].upper()}", order_no=order.order_no, amount_cents=refund_cents, points_amount=order.points_amount, reason="微信侧退款", status="processing", requested_by="wechat", ) db.add(refund) if refund_cents != refund.amount_cents: return _wechat_notify_response(request, success=False, message="refund amount mismatch") if result_code == 0: _complete_refund( db, order, refund, provider_refund_no=str(wechat_callback_value(payload, "WxRefundId") or refund_no), ) else: _complete_refund( db, order, refund, failure_reason=str(wechat_callback_value(payload, "RetMsg") or "微信退款失败"), ) db.commit() return _wechat_notify_response(request, success=True) # Irrelevant official-account events should not be retried as payment failures. return _wechat_notify_response(request, success=True) @router.get("/token/usage") def usage(authorization: str = Header(None), db: Session = Depends(get_db)): user = _require_user(authorization, db) rows = ( db.query( TokenUsage.avatar_id, TokenUsage.source, func.sum(TokenUsage.prompt_tokens), func.sum(TokenUsage.completion_tokens), func.sum(TokenUsage.total_tokens), func.count(TokenUsage.id), ) .filter(TokenUsage.user_id == user.id, TokenUsage.status == "completed") .group_by(TokenUsage.avatar_id, TokenUsage.source) .all() ) return ok([ { "avatarId": avatar_id, "source": source, "promptTokens": int(prompt_tokens or 0), "completionTokens": int(completion_tokens or 0), "totalTokens": int(total_tokens or 0), "requestCount": int(request_count or 0), } for avatar_id, source, prompt_tokens, completion_tokens, total_tokens, request_count in rows ])