table_game/backend/app/routers/user_order.py

336 lines
11 KiB
Python

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/orders/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 notify(request: Request):
# 获取请求的 body 数据
body = await request.body()
# 假设 wxpay.callback 是一个同步函数
result = wxpay.callback(request.headers, body)
logging.info(f"wxpay.callback 返回的结果: {result}")
if result and result.get('event_type') == 'TRANSACTION.SUCCESS':
resp = result.get('resource')
appid = resp.get('appid')
mchid = resp.get('mchid')
out_trade_no = resp.get('out_trade_no')
transaction_id = resp.get('transaction_id')
trade_type = resp.get('trade_type')
trade_state = resp.get('trade_state')
trade_state_desc = resp.get('trade_state_desc')
bank_type = resp.get('bank_type')
attach = resp.get('attach')
success_time = resp.get('success_time')
payer = resp.get('payer')
amount = resp.get('amount').get('total')
print(float(amount/100))
# 获取数据库连接
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()
# 更新订单状态
cursor.execute("""
UPDATE orders SET
order_status = 'completed',
payment_method = 'wechat',
paid_price = %s,
wx_transaction_id = %s,
settlement_time = NOW()
WHERE order_id = %s""",
(float(amount/100),transaction_id, order['order_id']))
connection.commit()
if cursor:
cursor.close()
if connection:
connection.close()
return JSONResponse(content={'code': 'SUCCESS', 'message': '成功'}, status_code=200)
else:
return JSONResponse(content={'code': 'FAILED', 'message': '失败'}, status_code=500)