deliveryman-api/app/api/endpoints/merchant_order.py
2025-03-31 17:14:57 +08:00

743 lines
25 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from sqlalchemy import func
from typing import List, Optional
from app.models.merchant_order import (
MerchantOrderDB,
MerchantOrderCreate,
MerchantOrderInfo,
MerchantOrderStatus
)
from app.models.address import AddressDB, AddressInfo
from app.models.merchant_product import MerchantProductDB, MerchantProductInfo
from app.models.database import get_db
from app.api.deps import get_current_user, get_admin_user, get_merchant_user
from app.models.user import UserDB
from app.core.response import success_response, error_response, ResponseModel
from datetime import datetime, timezone
from app.models.merchant import MerchantDB, MerchantInfo
from app.models.point import PointRecordDB
from app.core.account import AccountManager
from app.core.wechat import WeChatClient
from pydantic import BaseModel
from app.core.config import settings
from app.core.utils import CommonUtils
from sqlalchemy.sql import text
import qrcode
import io
from fastapi import UploadFile
from app.core.qcloud import qcloud_manager
from app.core.imageprocessor import process_image, ImageFormat
from starlette.datastructures import Headers
from app.models.merchant_order import MerchantOrderVerify
from app.core.point_manager import PointManager, PointRecordType
from app.models.merchant_product import DeliveryType, DeliveryTimeType
from datetime import timedelta
from fastapi import BackgroundTasks
from app.core.mpmessage import sent_merchant_order_status_change_message
from sqlalchemy.orm import joinedload
router = APIRouter()
@router.post("", response_model=ResponseModel)
async def create_merchant_order(
background_tasks: BackgroundTasks,
order: MerchantOrderCreate,
db: Session = Depends(get_db),
current_user: UserDB = Depends(get_current_user)
):
"""创建商家订单"""
# 检查商品是否存在
product = db.query(MerchantProductDB).filter(
MerchantProductDB.id == order.merchant_product_id
).first()
if not product:
return error_response(code=404, message="商品不存在")
# 检查限购
if product.purchase_limit > 0 and order.qty > product.purchase_limit:
return error_response(
code=400,
message=f"该商品限购 {product.purchase_limit}"
)
order_id = CommonUtils.generate_order_id('M')
verify_code = CommonUtils.generate_verify_code()
# 创建订单
pay_amount = float(product.sale_price) * order.qty
gift_points = int(pay_amount * float(product.gift_points_rate) / 100 * settings.POINT_RATIO)
db_order = MerchantOrderDB(
order_id=order_id,
user_id=current_user.userid,
merchant_id=product.merchant_id, # 从产品中获取商家ID
merchant_product_id=order.merchant_product_id,
unit_price=product.sale_price,
qty=order.qty,
address_id=order.address_id,
order_amount=pay_amount,
pay_amount=pay_amount,
gift_points=gift_points,
status=MerchantOrderStatus.CREATED,
order_verify_code=verify_code,
# 复制商品快照数据
product_delivery_type=product.delivery_type,
product_pickup_place=product.pickup_place,
product_pickup_time_from=product.pickup_time_from,
product_pickup_time_to=product.pickup_time_to,
product_delivery_time_type=product.delivery_time_type,
product_delivery_date=product.delivery_date
)
try:
db.add(db_order)
db.commit()
db.refresh(db_order)
# 发送商家订单创建成功消息
if current_user.mp_openid:
data={
"character_string1": order_id,
"thing2": product.name,
"amount3": pay_amount,
"time13": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
}
background_tasks.add_task(
sent_merchant_order_status_change_message,
openid=current_user.mp_openid,
template_id=settings.MERCHANT_ORDER_CREATED_TEMPLATE_ID,
data=data,
orderid=order_id
)
return success_response(data=MerchantOrderInfo.model_validate(db_order))
except Exception as e:
db.rollback()
return error_response(code=500, message=f"创建订单失败: {str(e)}")
@router.get("/merchant", response_model=ResponseModel)
async def get_merchant_orders(
skip: int = 0,
limit: int = 20,
delivery_type: Optional[DeliveryType] = None,
delivery_time_type: Optional[DeliveryTimeType] = None,
status: Optional[MerchantOrderStatus] = None,
db: Session = Depends(get_db),
merchant_user: UserDB = Depends(get_merchant_user)
):
"""获取商家订单列表"""
merchant = db.query(MerchantDB).filter(
MerchantDB.user_id == merchant_user.userid
).first()
if not merchant:
return error_response(code=404, message="商家不存在")
query = db.query(MerchantOrderDB).filter(
MerchantOrderDB.merchant_id == merchant.id
).options(
joinedload(MerchantOrderDB.merchant_product),
joinedload(MerchantOrderDB.merchant),
joinedload(MerchantOrderDB.address)
).order_by(
MerchantOrderDB.create_time.desc()
)
if delivery_type:
query = query.filter(
MerchantOrderDB.product_delivery_type == delivery_type
)
if delivery_time_type:
query = query.filter(
MerchantOrderDB.product_delivery_time_type == delivery_time_type
)
if status:
query = query.filter(
MerchantOrderDB.status == status
)
total = query.count()
orders = query.offset(skip).limit(limit).all()
result = []
for order in orders:
result.append({
"order" : MerchantOrderInfo.model_validate(order),
"product" : MerchantProductInfo.model_validate(order.merchant_product),
"merchant" : MerchantInfo.model_validate(order.merchant),
"address" : AddressInfo.model_validate(order.address)
})
return success_response(data={
"total": total,
"items": result
})
@router.get("/user", response_model=ResponseModel)
async def get_user_orders(
skip: int = 0,
limit: int = 20,
db: Session = Depends(get_db),
current_user: UserDB = Depends(get_current_user)
):
"""获取用户的订单列表"""
query = db.query(MerchantOrderDB).filter(
MerchantOrderDB.user_id == current_user.userid
).order_by(
MerchantOrderDB.create_time.desc()
)
total = query.count()
orders = query.offset(skip).limit(limit).all()
result = []
for order in orders:
product = db.query(MerchantProductDB).filter(
MerchantProductDB.id == order.merchant_product_id
).first()
merchant = db.query(MerchantDB).filter(
MerchantDB.id == product.merchant_id
).first()
result.append({
"order" : MerchantOrderInfo.model_validate(order),
"product" : MerchantProductInfo.model_validate(product),
"merchant" : MerchantInfo.model_validate(merchant)
})
return success_response(data={
"total": total,
"items": result
})
class MerchantCancelOrderRequest(BaseModel):
reason: str
@router.put("/{order_id}/merchant/cancel", response_model=ResponseModel)
async def merchant_cancel_order(
order_id: str,
request: MerchantCancelOrderRequest,
db: Session = Depends(get_db),
merchant_user: UserDB = Depends(get_merchant_user)
):
"""取消订单"""
merchant = db.query(MerchantDB).filter(
MerchantDB.user_id == merchant_user.userid
).first()
if not merchant:
return error_response(code=404, message="商家不存在")
order = db.query(MerchantOrderDB).filter(
MerchantOrderDB.order_id == order_id,
MerchantOrderDB.merchant_id == merchant.id
).first()
if not order:
return error_response(code=404, message="订单不存在")
try:
order.status = MerchantOrderStatus.REFUNDING
db.commit()
wechat = WeChatClient()
await wechat.apply_refund(
order_id=order.order_id,
total_amount=int(float(order.pay_amount) * 100) if not settings.DEBUG else 1, # 转换为分
reason="商家取消订单"
)
return success_response(data=MerchantOrderInfo.model_validate(order))
except Exception as e:
db.rollback()
return error_response(code=500, message=f"取消订单失败: {str(e)}")
@router.put("/{order_id}/user/cancel", response_model=ResponseModel)
async def user_cancel_order(
order_id: str,
db: Session = Depends(get_db),
current_user: UserDB = Depends(get_current_user)
):
"""取消订单"""
order = db.query(MerchantOrderDB).filter(
MerchantOrderDB.order_id == order_id,
MerchantOrderDB.user_id == current_user.userid
).first()
if not order:
return error_response(code=404, message="订单不存在")
try:
order.status = MerchantOrderStatus.REFUNDING
db.commit()
wechat = WeChatClient()
await wechat.apply_refund(
order_id=order.order_id,
total_amount=int(float(order.pay_amount) * 100) if not settings.DEBUG else 1, # 转换为分
reason="用户取消订单"
)
return success_response(data=MerchantOrderInfo.model_validate(order))
except Exception as e:
db.rollback()
return error_response(code=500, message=f"取消订单失败: {str(e)}")
@router.put("/{order_id}/complete", response_model=ResponseModel)
async def complete_order(
background_tasks: BackgroundTasks,
order_id: str,
db: Session = Depends(get_db),
merchant_user: UserDB = Depends(get_merchant_user)
):
"""商家完成订单"""
merchant = db.query(MerchantDB).filter(
MerchantDB.user_id == merchant_user.userid
).first()
if not merchant:
return error_response(code=404, message="商家不存在")
order = db.query(MerchantOrderDB).filter(
MerchantOrderDB.order_id == order_id,
MerchantOrderDB.merchant_id == merchant.id
).first()
if not order:
return error_response(code=404, message="订单不存在")
if merchant_user.userid != order.merchant.user_id:
return error_response(code=403, message="不是你的订单,无权限完成")
if order.status not in [MerchantOrderStatus.DELIVERING, MerchantOrderStatus.PICKUP_READY]:
return error_response(code=400, message="订单状态不正确")
try:
order.status = MerchantOrderStatus.COMPLETED
# 如果订单有赠送积分,增加积分
if order.gift_points > 0:
point_manager = PointManager(db)
point_manager.add_points(
user_id = order.user_id,
points = order.gift_points,
description = f"消费送蜂蜜",
order_id = order.order_id
)
# 对商家进行结算
account_manager = AccountManager(db)
settlement_amount = float(order.merchant_product.settlement_amount) * order.qty
if settlement_amount > 0:
account_manager.change_balance(
user_id=order.merchant.user_id,
amount=settlement_amount,
description=order.merchant_product.name,
transaction_id=order.order_id
)
# 更新商品销量
if order.merchant_product:
order.merchant_product.sold_total += order.qty
db.commit()
# 发送商家订单完成消息
user = db.query(UserDB).filter(
UserDB.userid == order.user_id
).first()
if user and user.mp_openid:
data={
"character_string7": order_id,
"thing5": order.merchant_product.name,
"character_string24": order.qty,
"time10": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
}
background_tasks.add_task(
sent_merchant_order_status_change_message,
openid=user.mp_openid,
template_id=settings.MERCHANT_ORDER_COMPLETED_TEMPLATE_ID,
data=data,
orderid=order_id
)
return success_response(data=MerchantOrderInfo.model_validate(order))
except Exception as e:
db.rollback()
return error_response(code=500, message=f"确认订单失败: {str(e)}")
@router.put("/{order_id}/accept", response_model=ResponseModel)
async def accept_order(
background_tasks: BackgroundTasks,
order_id: str,
db: Session = Depends(get_db),
merchant_user: UserDB = Depends(get_merchant_user)
):
"""商家接单订单"""
merchant = db.query(MerchantDB).filter(
MerchantDB.user_id == merchant_user.userid
).first()
if not merchant:
return error_response(code=404, message="商家不存在")
order = db.query(MerchantOrderDB).filter(
MerchantOrderDB.order_id == order_id,
MerchantOrderDB.merchant_id == merchant.id
).first()
if not order:
return error_response(code=404, message="订单不存在")
product = db.query(MerchantProductDB).filter(
MerchantProductDB.id == order.merchant_product_id
).first()
if not product:
return error_response(code=404, message="商品不存在")
if order.status != MerchantOrderStatus.PENDING:
return error_response(code=400, message="订单状态不正确")
try:
order.status = MerchantOrderStatus.DELIVERING
# 如果是及时达则设置配送截止时间为1小时后
if product.delivery_time_type == DeliveryTimeType.IMMEDIATE:
order.product_delivery_deadline_time = datetime.now() + timedelta(hours=1)
else:
order.product_delivery_deadline_time = None
db.commit()
# 发送商家订单接单消息
user = db.query(UserDB).filter(
UserDB.userid == order.user_id
).first()
if user and user.mp_openid:
data={
"thing1": product.name,
"amount2": order.pay_amount,
"character_string3": order.qty
}
background_tasks.add_task(
sent_merchant_order_status_change_message,
openid=user.mp_openid,
template_id=settings.MERCHANT_ORDER_ACCEPTED_TEMPLATE_ID,
data=data,
orderid=order_id
)
return success_response(data=MerchantOrderInfo.model_validate(order))
except Exception as e:
db.rollback()
return error_response(code=500, message=f"确认订单失败: {str(e)}")
@router.get("/merchant/scan_query_order/{verify_code}", response_model=ResponseModel)
async def scan_query_order(
verify_code: str,
db: Session = Depends(get_db),
merchant_user: UserDB = Depends(get_merchant_user)
):
"""查询待核销订单"""
merchant = db.query(MerchantDB).filter(
MerchantDB.user_id == merchant_user.userid
).first()
if not merchant:
return error_response(code=404, message="商家不存在")
order = db.query(
MerchantOrderDB
).filter(
MerchantOrderDB.order_verify_code == verify_code,
MerchantOrderDB.status == MerchantOrderStatus.PICKUP_READY,
MerchantOrderDB.merchant_id == merchant.id
).first()
if not order:
return error_response(code=404, message="订单不存在或已核销")
result = {
"order" : MerchantOrderInfo.model_validate(order),
"product" : MerchantProductInfo.model_validate(order.merchant_product),
"merchant" : MerchantInfo.model_validate(merchant)
}
return success_response(data=result)
@router.post("/merchant/verify_order", response_model=ResponseModel)
async def verify_order(
request: MerchantOrderVerify,
db: Session = Depends(get_db),
merchant_user: UserDB = Depends(get_merchant_user)
):
"""核销订单"""
# 查询订单及相关信息
order = db.query(MerchantOrderDB).filter(
MerchantOrderDB.order_verify_code == request.verify_code,
MerchantOrderDB.status == MerchantOrderStatus.PICKUP_READY
).first()
if not order:
return error_response(code=404, message="订单不存在或已核销")
if merchant_user.userid != order.merchant.user_id:
return error_response(code=403, message="不是你的订单,无权限核销")
try:
# 更新核销时间和核销用户
order.verify_time = datetime.now(timezone.utc)
order.verify_user_id = merchant_user.userid
order.status = MerchantOrderStatus.COMPLETED
# 如果有积分奖励,赠送积分
if order.gift_points > 0:
point_manager = PointManager(db)
point_manager.add_points(
user_id=order.user_id,
points=order.gift_points,
description=order.merchant_product.name,
order_id=order.order_id
)
# 对商家进行结算
account_manager = AccountManager(db)
settlement_amount = float(order.settlement_amount) * order.qty
if settlement_amount > 0:
account_manager.change_balance(
user_id=order.merchant.user_id,
amount=settlement_amount,
description=order.merchant_product.name,
transaction_id=order.order_id
)
# 更新商品销量
if order.merchant_product:
order.merchant_product.sold_total += order.qty
db.commit()
return success_response(
message="核销成功",
data=MerchantOrderInfo.model_validate(order)
)
except Exception as e:
db.rollback()
return error_response(code=500, message=f"核销失败: {str(e)}")
@router.post("/{order_id}/refund/apply", response_model=ResponseModel)
async def apply_refund(
order_id: str,
db: Session = Depends(get_db),
current_user: UserDB = Depends(get_current_user)
):
"""申请退款"""
order = db.query(MerchantOrderDB).filter(
MerchantOrderDB.order_id == order_id,
MerchantOrderDB.user_id == current_user.userid, # 只能申请自己的订单
MerchantOrderDB.status == MerchantOrderStatus.PENDING # 只有未核销的订单可以退款
).first()
if not order:
return error_response(code=404, message="订单不存在或状态不允许退款")
# 更新状态为退款中
order.status = MerchantOrderStatus.REFUNDING
try:
# 调用微信支付退款
wechat = WeChatClient()
await wechat.apply_refund(
order_id=order.order_id,
total_amount=int(float(order.pay_amount) * 100) if not settings.DEBUG else 1, # 转换为分
reason="用户申请退款"
)
db.commit()
return success_response(
message="退款申请成功",
data=MerchantOrderInfo.model_validate(order)
)
except Exception as e:
db.rollback()
return error_response(code=500, message=f"申请退款失败: {str(e)}")
@router.get("/{order_id}", response_model=ResponseModel)
async def get_order_detail(
order_id: str,
longitude: Optional[float] = None,
latitude: Optional[float] = None,
db: Session = Depends(get_db),
current_user: UserDB = Depends(get_current_user)
):
"""获取订单详情"""
order = db.query(MerchantOrderDB).filter(
MerchantOrderDB.order_id == order_id
).first()
if not order:
return error_response(code=404, message="订单不存在")
product = db.query(MerchantProductDB).filter(
MerchantProductDB.id == order.merchant_product_id
).first()
if not product:
return error_response(code=404, message="商品不存在")
merchant = db.query(MerchantDB).filter(
MerchantDB.id == product.merchant_id
).first()
if not merchant:
return error_response(code=404, message="商家不存在")
order_data = {
"order" : MerchantOrderInfo.model_validate(order),
"product" : MerchantProductInfo.model_validate(product),
"merchant" : MerchantInfo.model_validate(merchant)
}
if order.address_id:
address = db.query(AddressDB).filter(
AddressDB.id == order.address_id
).first()
order_data["address"] = AddressInfo.model_validate(address)
return success_response(data=order_data)
@router.post("/calculate-price", response_model=ResponseModel)
async def calculate_order_price(
order: MerchantOrderCreate,
db: Session = Depends(get_db),
current_user: UserDB = Depends(get_current_user)
):
"""计算订单价格
1. 获取商品信息和用户积分
2. 计算最高可抵扣金额
3. 返回最终支付金额
"""
# 查询商品信息
product = db.query(MerchantProductDB).filter(
MerchantProductDB.id == order.merchant_product_id
).first()
if not product:
return error_response(code=404, message="商品不存在")
pay_amount = float(product.sale_price) * order.qty
gift_points = pay_amount * float(product.gift_points_rate) / 100 * settings.POINT_RATIO
return success_response(data={
"gift_points": int(gift_points),
"amount": pay_amount
})
@router.get("/{order_id}/verify-qrcode", response_model=ResponseModel)
async def get_order_qrcode(
order_id: str,
db: Session = Depends(get_db),
current_user: UserDB = Depends(get_current_user)
):
"""获取订单核销二维码"""
# 查询订单
order = db.query(MerchantOrderDB).filter(
MerchantOrderDB.order_id == order_id
).first()
if not order:
return error_response(code=404, message="订单不存在")
# 如果已经有二维码,直接返回
if order.qrcode_url:
url = process_image(order.qrcode_url).thumbnail(800, 800).format(ImageFormat.WEBP).build()
return success_response(data={"qrcode_url":url})
try:
# 生成二维码
qr = qrcode.QRCode(
version=1,
error_correction=qrcode.constants.ERROR_CORRECT_L,
box_size=10,
border=1,
)
qr.add_data(order.order_verify_code)
qr.make(fit=True)
# 创建图片
img = qr.make_image(fill_color="black", back_color="white")
# 将图片转换为字节流
img_byte_arr = io.BytesIO()
img.save(img_byte_arr, format='PNG')
img_byte_arr.seek(0)
# 创建 UploadFile 对象
file = UploadFile(
file=img_byte_arr,
filename=f"qrcode_{order_id}.png",
headers=Headers({"content-type": "image/png"})
)
# 上传到腾讯云
url = await qcloud_manager.upload_file(file)
# 更新订单的二维码URL
order.qrcode_url = url
db.commit()
process_url = process_image(url).thumbnail(800, 800).format(ImageFormat.WEBP).build()
return success_response(data={"qrcode_url": process_url})
except Exception as e:
db.rollback()
return error_response(code=500, message=f"生成二维码失败: {str(e)}")
# 获取商家订单数据汇总
@router.get("/merchant/summary", response_model=ResponseModel)
async def get_merchant_order_summary(
db: Session = Depends(get_db),
merchant_user: UserDB = Depends(get_merchant_user)
):
"""获取商家订单数据汇总"""
# 查询商家订单数量
total = db.query(MerchantOrderDB).filter(
MerchantOrderDB.user_id == merchant_user.userid
).count()
today = datetime.now().date()
yesterday = today - timedelta(days=1)
today_start = datetime.combine(today, datetime.min.time())
today_end = datetime.combine(today, datetime.max.time())
yesterday_start = datetime.combine(yesterday, datetime.min.time())
yesterday_end = datetime.combine(yesterday, datetime.max.time())
# 查询商家昨日、今日订单数量
yesterday_total = db.query(MerchantOrderDB).filter(
MerchantOrderDB.user_id == merchant_user.userid,
MerchantOrderDB.status == MerchantOrderStatus.COMPLETED,
MerchantOrderDB.create_time.between(yesterday_start, yesterday_end)
).count()
today_total = db.query(MerchantOrderDB).filter(
MerchantOrderDB.user_id == merchant_user.userid,
MerchantOrderDB.status == MerchantOrderStatus.COMPLETED,
MerchantOrderDB.create_time.between(today_start, today_end)
).count()
return success_response(data={
"total": total,
"yesterday_total": yesterday_total,
"today_total": today_total
})