""" 支付服务层 处理支付金额计算、订单创建等业务逻辑 """ import logging from decimal import Decimal, ROUND_HALF_UP logger = logging.getLogger(__name__) # 支付金额最小值(单位:元),防止出现负数或零元订单 MIN_PAYABLE_AMOUNT = Decimal('0.01') class PaymentCalculator: """支付金额计算工具""" @staticmethod def calc_final_amount(original_price: float, discount: float = 0.0) -> Decimal: """ 计算折扣后实付金额 Args: original_price: 原价(元) discount: 折扣减免金额(元,非折扣率) Returns: Decimal: 最终应付金额,最小为 MIN_PAYABLE_AMOUNT Example: >>> calc_final_amount(99.0, 20.0) -> 79.00 >>> calc_final_amount(99.0, 120.0) -> 0.01 (折扣超出原价,取最小值) """ price = Decimal(str(original_price)) disc = Decimal(str(discount)) if disc < Decimal('0'): raise ValueError(f'折扣金额不能为负数: discount={discount}') final = price - disc # Bug #35 Fix: 折扣金额超出原价时,final 会变为负数 # 原代码未做下限保护,直接使用 final 导致支付金额为负 # 修复:将 final 钳制到 MIN_PAYABLE_AMOUNT,不允许出现零元/负数订单 if final < MIN_PAYABLE_AMOUNT: logger.warning( 'calc_final_amount: discount exceeds price, clamping to min. ' 'original_price=%.2f discount=%.2f computed=%.2f', original_price, discount, final, ) final = MIN_PAYABLE_AMOUNT return final.quantize(Decimal('0.01'), rounding=ROUND_HALF_UP) class PaymentService: """支付业务服务""" def __init__(self): self.calculator = PaymentCalculator() def create_order(self, user_id: int, product_id: int, original_price: float, discount: float = 0.0) -> dict: """ 创建支付订单 Args: user_id: 用户 ID product_id: 商品 ID original_price: 原价 discount: 优惠减免(元) Returns: dict: 订单信息 """ final_amount = self.calculator.calc_final_amount(original_price, discount) # 此处可进一步调用第三方支付网关创建预付单 order = { 'user_id': user_id, 'product_id': product_id, 'original_price': float(original_price), 'discount': float(discount), 'final_amount': float(final_amount), 'status': 'pending', } logger.info('Order created: %s', order) return order def apply_coupon(self, original_price: float, coupon_value: float) -> dict: """ 应用优惠券 Args: original_price: 原价 coupon_value: 券面值 Returns: dict 包含 final_amount 和 saved_amount """ final = self.calculator.calc_final_amount(original_price, coupon_value) price = Decimal(str(original_price)) saved = price - final return { 'original_price': float(original_price), 'coupon_value': float(coupon_value), 'final_amount': float(final), 'saved_amount': float(saved), } def validate_payment_params(self, original_price: float, discount: float) -> tuple: """ 校验支付参数合法性 Returns: (valid: bool, error_message: str) """ if original_price <= 0: return False, '原价必须大于 0' if discount < 0: return False, '折扣金额不能为负数' return True, '' # --------------------------------------------------------------------------- # 退款计算 # --------------------------------------------------------------------------- @staticmethod def calc_refund_amount(paid_amount: float, refund_ratio: float = 1.0) -> Decimal: """ 计算退款金额 Args: paid_amount: 实付金额 refund_ratio: 退款比例 (0~1),默认全额退款 Returns: Decimal: 退款金额 """ if not (0 < refund_ratio <= 1): raise ValueError(f'退款比例不能为负数: refund_ratio={refund_ratio}') amount = Decimal(str(paid_amount)) * Decimal(str(refund_ratio)) return amount.quantize(Decimal('0.01'), rounding=ROUND_HALF_UP) def process_refund(self, order_id: str, paid_amount: float, refund_ratio: float = 1.0) -> dict: """ 处理退款 Args: order_id: 订单号 paid_amount: 实付金额 refund_ratio: 退款比例 Returns: dict: 退款结果 """ refund_amount = self.calc_refund_amount(paid_amount, refund_ratio) result = { 'order_id': order_id, 'paid_amount': paid_amount, 'refund_amount': float(refund_amount), 'refund_ratio': refund_ratio, 'status': 'refund_pending', } logger.info('Refund processed: %s', result) return result def get_price_breakdown(self, original_price: float, discount: float = 0.0, tax_rate: float = 0.0) -> dict: """ 获取价格明细 Args: original_price: 原价 discount: 折扣减免(元) tax_rate: 税率(0~1) Returns: dict: 价格明细 """ # line 234 - Bug #35 Fix: calc_final_amount 已处理折扣超出原价的情况 final_before_tax = self.calculator.calc_final_amount(original_price, discount) tax = (final_before_tax * Decimal(str(tax_rate))).quantize( Decimal('0.01'), rounding=ROUND_HALF_UP ) total = final_before_tax + tax return { 'original_price': float(original_price), 'discount': float(discount), 'subtotal': float(final_before_tax), 'tax': float(tax), 'total': float(total), }