diff --git a/app/api/endpoints/order.py b/app/api/endpoints/order.py index 75bbc2e..ab7a3dd 100644 --- a/app/api/endpoints/order.py +++ b/app/api/endpoints/order.py @@ -13,7 +13,8 @@ from app.models.order import ( OrderStatus, OrderCancel, OrderComplete, - OrderPriceResult + OrderPriceResult, + RefundOrderAmountRequest ) from app.models.order_additional_fee import OrderAdditionalFeeDB, OrderAdditionalFeeInfo, AdditionalFeeResult from app.models.database import get_db @@ -1571,7 +1572,61 @@ async def get_orders( except Exception as e: logging.exception(f"获取订单列表失败: {str(e)}") return error_response(code=500, message=f"获取订单列表失败: {str(e)}") - + + +@router.put("/admin/refund_order_amount", response_model=ResponseModel) +async def refund_order_amount( + request: RefundOrderAmountRequest, + db: Session = Depends(get_db), + admin_user: UserDB = Depends(get_admin_user) +): + """减少订单金额""" + try: + order = db.query(ShippingOrderDB).filter(ShippingOrderDB.orderid == request.order_id).first() + if not order: + return error_response(code=404, message="订单不存在") + + # 检查退款金额是否大于订单支付金额 + if request.amount > order.final_amount: + return error_response(code=400, message="退款金额不能大于订单金额") + + order.refund_amount = request.amount + order.refund_reason = request.reason + order.refund_user_id = admin_user.userid + + order.final_amount -= request.amount + + # 如果订单已经完成,则扣减配送员收益 + order.delivery_share = calculate_delivery_share(order, db) + if order.status == OrderStatus.COMPLETED: + # 扣减配送员收益 + sharing = db.query(CommunityProfitSharing).filter( + CommunityProfitSharing.community_id == order.address_community_id + ).first() + if sharing: + reduct_share = -round(request.amount * (float(sharing.delivery_rate) / 100.0), 2) + manager = AccountManager(db) + manager.change_balance( + order.deliveryman_user_id, + reduct_share, + f"配送订单退款", + order.orderid + ) + + wechat = WeChatClient() + await wechat.apply_refund( + order_id=order.orderid, + total_amount=int(float(request.amount) * 100) if not settings.DEBUG else 1, # 转换为分 + reason="后台退款" + ) + + db.commit() + return success_response(message="减少订单金额成功") + except Exception as e: + db.rollback() + logging.exception(f"减少订单金额失败: {str(e)}") + return error_response(code=500, message=f"减少订单金额失败: {str(e)}") + @router.get("/admin/list", response_model=ResponseModel) async def get_admin_orders( @@ -1654,6 +1709,9 @@ async def get_admin_orders( "complete_images": order.optimized_complete_images, "completed_time": order.completed_time, "final_amount": order.final_amount, + "refund_amount": order.refund_amount, + "refund_reason": order.refund_reason, + "refund_user_id": order.refund_user_id, "packages": package_list, "sub_orders": [PointProductOrderInfo.model_validate(sub_order) for sub_order in sub_orders], "address": { diff --git a/app/api/endpoints/wechat.py b/app/api/endpoints/wechat.py index a87c825..11a9c98 100644 --- a/app/api/endpoints/wechat.py +++ b/app/api/endpoints/wechat.py @@ -26,7 +26,7 @@ import logging from app.core.security import get_password_hash from app.models.order_additional_fee import OrderAdditionalFeeDB, AdditionalFeeResult from app.api.endpoints.order import calculate_delivery_share -from app.models.wechat_payment import WechatPaymentRecordDB +from app.models.wechat_payment import WechatPaymentRecordDB, WechatRefundRecordDB from app.core.wecombot import WecomBot from app.models.order import OrderInfo @@ -400,14 +400,52 @@ async def refund_notify( # 获取退款信息 out_trade_no = data.get("out_trade_no") # 订单号 + out_refund_no = data.get("out_refund_no") # 退款单号 + refund_id = data.get("refund_id") # 微信退款单号 + transaction_id = data.get("transaction_id") # 微信支付交易号 status = data.get("refund_status") success_time = data.get("success_time") + refund_amount = data.get("amount", {}).get("refund") + total_amount = data.get("amount", {}).get("total") + user_received_account = data.get("user_received_account") + refund_reason = "退款" # 默认退款原因 + + # 保存退款记录数据 + refund_record = WechatRefundRecordDB( + out_trade_no=out_trade_no, + out_refund_no=out_refund_no, + refund_id=refund_id, + transaction_id=transaction_id, + refund_status=status, + success_time=datetime.fromisoformat(success_time.replace('Z', '+00:00')) if success_time else None, + refund_amount=float(refund_amount) / 100 if refund_amount else 0, + total_amount=float(total_amount) / 100 if total_amount else 0, + user_received_account=user_received_account, + refund_reason=refund_reason, + raw_data=data + ) + db.add(refund_record) if not all([out_trade_no, status]) or status != "SUCCESS": return error_response(code=400, message="缺少必要的退款信息或退款未成功") # 根据订单类型处理退款 - if out_trade_no.startswith('M'): # 商家商品订单 + if out_trade_no.startswith('D'): # 配送订单 + order = db.query(ShippingOrderDB).filter( + ShippingOrderDB.orderid == out_trade_no + ).first() + + if not order: + return error_response(code=404, message="订单不存在或状态不正确") + + # 更新退款相关字段 + if refund_amount: + order.refund_amount = refund_amount + + # 记录退款时间 + order.refund_time = datetime.fromisoformat(success_time.replace('Z', '+00:00')) + + elif out_trade_no.startswith('M'): # 商家商品订单 order = db.query(MerchantOrderDB).filter( MerchantOrderDB.order_id == out_trade_no, MerchantOrderDB.status == MerchantOrderStatus.REFUNDING @@ -418,7 +456,7 @@ async def refund_notify( # 更新订单状态 order.status = MerchantOrderStatus.CANCELLED - order.refund_transaction_id = data.get("transaction_id") + order.refund_transaction_id = transaction_id order.refund_time = datetime.fromisoformat(success_time.replace('Z', '+00:00')) elif out_trade_no.startswith('P'): # 商家在线买单 diff --git a/app/models/order.py b/app/models/order.py index ac4c97b..3f69700 100644 --- a/app/models/order.py +++ b/app/models/order.py @@ -93,6 +93,12 @@ class ShippingOrderDB(Base): prepay_id = Column(String(64)) # 微信支付预支付ID pay_time = Column(DateTime(timezone=True)) # 支付时间 transaction_id = Column(String(64)) # 微信支付交易号 + + # 退款相关字段 + refund_amount = Column(Float, default=0) # 退款金额 + refund_reason = Column(String(200), nullable=True) # 退款原因 + refund_user_id = Column(Integer, ForeignKey("users.userid"), nullable=True) # 退款操作人 + refund_time = Column(DateTime(timezone=True), nullable=True) # 退款时间 @property def optimized_complete_images(self): @@ -109,7 +115,7 @@ class ShippingOrderDB(Base): @property def original_amount_with_additional_fee(self): return self.original_amount + self.additional_fee_amount - + class ShippingOrderPackageDB(Base): __tablename__ = "shipping_order_packages" @@ -196,11 +202,11 @@ class OrderInfo(BaseModel): completed_time: Optional[datetime] = None is_first_order: bool - # def __init__(self, **data): - # super().__init__(**data) - # # 将逗号分隔的图片URL字符串转换为列表 - # if self.complete_images and isinstance(self.complete_images, str): - # self.complete_images = self.complete_images.split(",") + # 退款相关字段 + refund_amount: float = 0 + refund_reason: Optional[str] = None + refund_user_id: Optional[int] = None + refund_time: Optional[datetime] = None class Config: from_attributes = True @@ -241,4 +247,9 @@ class OrderPriceResult(BaseModel): price_info: OrderPriceInfo used_coupon_id: Optional[int] = None # 使用的优惠券ID used_points: Optional[int] = None # 使用的积分数 - price_detail_text: Optional[str] = None # 价格详情文本 \ No newline at end of file + price_detail_text: Optional[str] = None # 价格详情文本 + +class RefundOrderAmountRequest(BaseModel): + order_id: str + amount: float + reason: str \ No newline at end of file diff --git a/app/models/wechat_payment.py b/app/models/wechat_payment.py index 0f2aa23..f8916ea 100644 --- a/app/models/wechat_payment.py +++ b/app/models/wechat_payment.py @@ -46,5 +46,46 @@ class WechatPaymentRecordInfo(BaseModel): create_time: datetime update_time: Optional[datetime] = None + class Config: + from_attributes = True + +class WechatRefundRecordDB(Base): + """微信退款记录表""" + __tablename__ = "wechat_refund_records" + + id = Column(Integer, primary_key=True, autoincrement=True) + out_trade_no = Column(String(32), nullable=False, index=True, comment='商户订单号') + out_refund_no = Column(String(64), nullable=False, index=True, comment='商户退款单号') + refund_id = Column(String(64), index=True, comment='微信退款单号') + transaction_id = Column(String(64), index=True, comment='微信支付交易号') + refund_status = Column(String(32), comment='退款状态') + success_time = Column(DateTime(timezone=True), comment='退款成功时间') + refund_amount = Column(DECIMAL(10, 2), comment='退款金额') + total_amount = Column(DECIMAL(10, 2), comment='订单总金额') + user_received_account = Column(String(256), comment='退款入账账户') + currency = Column(String(16), default='CNY', comment='货币类型') + refund_reason = Column(String(256), comment='退款原因') + raw_data = Column(JSON, comment='原始返回数据') + create_time = Column(DateTime(timezone=True), server_default=func.now(), comment='创建时间') + update_time = Column(DateTime(timezone=True), onupdate=func.now(), comment='更新时间') + +# Pydantic 模型 +class WechatRefundRecordInfo(BaseModel): + id: int + out_trade_no: str + out_refund_no: str + refund_id: Optional[str] = None + transaction_id: Optional[str] = None + refund_status: Optional[str] = None + success_time: Optional[datetime] = None + refund_amount: Optional[float] = None + total_amount: Optional[float] = None + user_received_account: Optional[str] = None + currency: str = 'CNY' + refund_reason: Optional[str] = None + raw_data: Optional[Dict[str, Any]] = None + create_time: datetime + update_time: Optional[datetime] = None + class Config: from_attributes = True \ No newline at end of file diff --git a/app/sql/v1.1.sql b/app/sql/v1.1.sql index a9737b0..ba20da5 100644 --- a/app/sql/v1.1.sql +++ b/app/sql/v1.1.sql @@ -176,4 +176,13 @@ ADD UNIQUE INDEX uix_fee_order_id (fee_order_id); ALTER TABLE order_additional_fees ADD COLUMN transaction_id VARCHAR(64) NULL COMMENT '微信支付交易号'; +--====FINISH 4.5==== + +-- 为 shipping_orders 表添加 refund_amount 等字段 +ALTER TABLE shipping_orders +ADD COLUMN refund_amount FLOAT DEFAULT 0, +ADD COLUMN refund_reason VARCHAR(200), +ADD COLUMN refund_user_id INT, +ADD CONSTRAINT fk_refund_user_id FOREIGN KEY (refund_user_id) REFERENCES users(userid); + diff --git a/jobs.sqlite b/jobs.sqlite index 07af01e..82c15ec 100644 Binary files a/jobs.sqlite and b/jobs.sqlite differ