from pydantic import BaseModel, Field from fastapi import APIRouter, HTTPException, Request from fastapi.responses import JSONResponse from pydantic import BaseModel import logging from wechatpayv3 import WeChatPay, WeChatPayType from ..utils.jwt_handler import verify_token from string import ascii_letters, digits from datetime import datetime import json from random import sample import configparser from ..services.user_order_service import ( get_user_active_order, create_user_order, complete_user_order, get_earliest_pending_order, get_order_detail_with_points, preview_price_adjustment ) from ..db import get_connection from decimal import Decimal import time import uuid config = configparser.ConfigParser() config.read('backend/config.conf') points_rate = Decimal(config.get('price', 'points_rate')) get_points_rate = Decimal(config.get('price', 'get_points_rate')) logging.basicConfig(filename='demo.log', level=logging.DEBUG, filemode='a', format='%(asctime)s - %(process)s - %(levelname)s: %(message)s') LOGGER = logging.getLogger("demo") MCHID = config.get('wechat', 'mchid') with open('backend/cert/apiclient_key.pem') as f: PRIVATE_KEY = f.read() CERT_SERIAL_NO = config.get('wechat','cert_serial_no') APIV3_KEY = config.get('wechat','apiv3_key') APPID = config.get('wechat','appid') NOTIFY_URL = 'https://table-game-backend.miniprogram.ahaostudio.tech/user/order/paystatus/' CERT_DIR = './cert' PARTNER_MODE = False PROXY = None TIMEOUT = (10, 30) wxpay = WeChatPay( wechatpay_type=WeChatPayType.MINIPROG, mchid=MCHID, private_key=PRIVATE_KEY, cert_serial_no=CERT_SERIAL_NO, apiv3_key=APIV3_KEY, appid=APPID, notify_url=NOTIFY_URL, cert_dir=CERT_DIR, logger=LOGGER, partner_mode=PARTNER_MODE, proxy=PROXY, timeout=TIMEOUT ) router = APIRouter() class ActiveOrderRequest(BaseModel): token: str class CreateOrderRequest(BaseModel): token: str table_id: int num_players: int class CompleteOrderRequest(BaseModel): order_id: int token: str @router.post("/active") def get_active_order(request: ActiveOrderRequest): return get_user_active_order(request.token) @router.post("/create") def create_order(request: CreateOrderRequest): return create_user_order( request.token, request.table_id, request.num_players ) @router.post("/complete") def complete_order(request: CompleteOrderRequest): return complete_user_order(request.token, request.order_id) class PendingOrderRequest(BaseModel): token: str class OrderDetailRequest(BaseModel): order_id: int token: str @router.post("/pending") def get_pending_order(request: PendingOrderRequest): """获取用户最早的pending订单""" return get_earliest_pending_order(request.token) @router.post("/details") def get_order_details(request: OrderDetailRequest): """获取订单详情及用户积分""" return get_order_detail_with_points(request.token, request.order_id) class PricePreviewRequest(BaseModel): token: str order_id: int used_points: int = Field(..., ge=0) @router.post("/preview_price") def preview_price(request: PricePreviewRequest): """价格调整预览接口""" return preview_price_adjustment( request.token, request.order_id, request.used_points ) class OrderPayRequest(BaseModel): token: str order_id: int used_points: int = None coupon_id: int = None @router.post("/pay/") async def order_pay(request: OrderPayRequest): token = request.token order_id = request.order_id if not token or not order_id: raise HTTPException(status_code=400, detail="缺少必要参数") try: connection = get_connection() payload = verify_token(token) cursor = connection.cursor(dictionary=True) cursor.execute("SELECT user_id, wx_openid, points FROM users WHERE phone_number = %s", (payload["sub"],)) user = cursor.fetchone() if not user: raise HTTPException(status_code=404, detail="用户不存在") user_id = user['user_id'] wx_openid = user['wx_openid'] points = user['points'] if not wx_openid: raise HTTPException(status_code=400, detail="用户未绑定微信,无法进行支付") cursor.execute(""" SELECT * FROM orders WHERE order_id = %s AND user_id = %s AND order_status = 'pending' FOR UPDATE""", (request.order_id, user_id)) order = cursor.fetchone() if not order: raise HTTPException(status_code=404, detail="订单不存在或不可支付") final_price = order['payable_price'] # 计算最终价格 used_points = request.used_points or 0 if used_points > 0: if points < used_points: raise HTTPException(400, "积分不足") points_value = used_points * points_rate else: points_value = 0 coupon_value = 0 if request.coupon_id != -1: cursor.execute(""" SELECT coupon_type, discount, min_amount, is_used FROM coupons WHERE coupon_id = %s AND expiration_date > NOW() FOR UPDATE """, (request.coupon_id,)) coupon = cursor.fetchone() if not coupon: raise HTTPException(400, "无效的优惠券") if coupon['is_used']: raise HTTPException(400, "优惠券已被使用") if coupon['min_amount'] and final_price < coupon['min_amount']: raise HTTPException(400, f"订单金额不足{coupon['min_amount']}元") if coupon['coupon_type'] == 'discount': coupon_value = final_price * coupon['discount'] elif coupon['coupon_type'] == 'cash': coupon_value = coupon['discount'] else: raise HTTPException(400, "未知优惠券类型") # 标记优惠券已使用 cursor.execute(""" UPDATE coupons SET is_used = TRUE WHERE coupon_id = %s """, (request.coupon_id,)) payable_price = max(final_price - points_value - coupon_value, Decimal('0')) payable_price = int(payable_price * 100) # 转换为分 out_trade_no = ''.join(sample(ascii_letters + digits, 8)) cursor.execute("SELECT game_table_number FROM game_tables WHERE table_id = %s", (order['game_table_id'],)) game_table = cursor.fetchone() game_table_number = game_table['game_table_number'] if game_table else '未知' description = f"即墨区小鲨桌游店-第{game_table_number}桌-订单结算" code, message = wxpay.pay( description=description, out_trade_no=out_trade_no, amount={'total': payable_price}, payer={'openid': wx_openid}, pay_type=WeChatPayType.MINIPROG ) if code == 200: try: message_dict = json.loads(message) prepay_id = message_dict.get('prepay_id') if prepay_id: timestamp = str(int(time.time())) noncestr = str(uuid.uuid4()).replace('-', '') package = f'prepay_id={prepay_id}' sign = wxpay.sign(data=[APPID, timestamp, noncestr, package]) signtype = 'RSA' pay_params = { "appId": APPID, "timeStamp": timestamp, "nonceStr": noncestr, "package": package, "signType": signtype, "paySign": sign } cursor.execute( "UPDATE orders SET out_trade_no = %s WHERE order_id = %s", (out_trade_no, order_id) ) if request.coupon_id != -1: cursor.execute(""" UPDATE orders SET coupon_id = %s, coupon_type = %s, coupon_value = %s WHERE order_id = %s""", (request.coupon_id, coupon['coupon_type'], coupon_value, request.order_id)) connection.commit() return {"code": "success", "message": pay_params} else: return {"code": "error", "message": "未获取到 prepay_id"} except json.JSONDecodeError: return {"code": "error", "message": f"无法解析返回的消息: {message}"} else: return {"code": "error", "message": message} except Exception as e: print(e) LOGGER.error(f"处理支付请求时发生错误:{e}") raise HTTPException(status_code=500, detail="处理支付请求时发生错误") finally: if cursor: cursor.close() if connection: connection.close() @router.post("/paystatus/") async def wxpay_notify(request: Request): connection = None cursor = None try: # 获取回调数据 headers = dict(request.headers) body_bytes = await request.body() # 验证微信支付签名 if not wxpay.verify(headers, body_bytes): logging.warning("签名验证失败") return JSONResponse(content={"code": "FAIL", "message": "签名验证失败"}, status_code=400) # 解密回调数据 result = wxpay.decrypt_callback(headers, body_bytes) out_trade_no = result.get('out_trade_no') transaction_id = result.get('transaction_id') if not out_trade_no or not transaction_id: return JSONResponse(content={"code": "FAIL", "message": "缺少必要参数"}, status_code=400) # 获取数据库连接 connection = get_connection() cursor = connection.cursor(dictionary=True) # 查询订单并锁定 cursor.execute(""" SELECT order_id, payable_price FROM orders WHERE out_trade_no = %s FOR UPDATE""", (out_trade_no,)) order = cursor.fetchone() if not order: logging.error(f"订单不存在: {out_trade_no}") return JSONResponse(content={"code": "FAIL", "message": "订单不存在"}, status_code=404) # 验证金额(示例) callback_total = int(result.get('amount', {}).get('total', 0)) payable_cents = int(order['payable_price'] * 100) if callback_total != payable_cents: logging.error(f"金额不匹配: 订单应支付{payable_cents}分,回调收到{callback_total}分") return JSONResponse(content={"code": "FAIL", "message": "金额不匹配"}, status_code=400) # 更新订单状态 cursor.execute(""" UPDATE orders SET order_status = 'completed', payment_method = 'wechat', wx_transaction_id = %s, settlement_time = NOW() WHERE order_id = %s""", (transaction_id, order['order_id'])) connection.commit() # 返回微信要求的成功响应 return JSONResponse(content={"code": "SUCCESS", "message": "OK"}) except Exception as e: logging.error(f"回调处理异常: {str(e)}", exc_info=True) if connection: connection.rollback() return JSONResponse(content={"code": "FAIL", "message": "系统错误"}, status_code=500) finally: if cursor: cursor.close() if connection: connection.close()