350 lines
12 KiB
Python
350 lines
12 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/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()
|