payment_service.py 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. # app/services/payment_service.py
  2. import time
  3. import stripe
  4. import random
  5. from typing import List,Dict
  6. from decimal import Decimal, ROUND_HALF_UP
  7. from datetime import datetime, timedelta
  8. from sqlalchemy.orm import Session
  9. from app.core.biz_exception import NotFoundError, PermissionDeniedError, BizLogicError
  10. from app.models.order import VasOrder
  11. from app.models.product import VasProduct
  12. from app.models.payment import VasPayment
  13. from app.models.payment_provider import VasPaymentProvider
  14. from app.models.payment_qr import VasPaymentQR
  15. from app.schemas.payment import VasPaymentCreate
  16. class PaymentService:
  17. @staticmethod
  18. def create_payment(db: Session, payload: VasPaymentCreate, rate_table: Dict):
  19. with db.begin():
  20. # ① 锁住订单,防止并发创建 payment
  21. order = (
  22. db.query(VasOrder)
  23. .filter(VasOrder.id == payload.order_id)
  24. .with_for_update()
  25. .one()
  26. )
  27. # ② 是否已有进行中的 payment
  28. active_payment = (
  29. db.query(VasPayment)
  30. .filter(
  31. VasPayment.order_id == order.id,
  32. VasPayment.status == "pending"
  33. )
  34. .first()
  35. )
  36. if active_payment:
  37. raise BizLogicError("Order already has active payment")
  38. if payload.provider in ("wechat", "alipay"):
  39. return success(
  40. data = PaymentService.create_offline_payment(
  41. db=db,
  42. order=order,
  43. provider_name=payload.provider,
  44. rate_table=rate_table,
  45. )
  46. )
  47. if payload.provider == "stripe":
  48. return success(
  49. data = PaymentService.create_stripe_payment(
  50. db=db,
  51. order=order,
  52. rate_table=rate_table,
  53. )
  54. )
  55. raise BizLogicError("Unsupported provider")
  56. @staticmethod
  57. def create_offline_payment(db, order, provider_name: str, rate_table: dict):
  58. payment = (
  59. PaymentService._create_wechat_payment(db, order)
  60. if provider_name == "wechat"
  61. else PaymentService._create_alipay_payment(db, order)
  62. )
  63. provider = db.query(VasPaymentProvider).filter(
  64. VasPaymentProvider.enabled == 1,
  65. VasPaymentProvider.name == provider_name
  66. ).first()
  67. qrs = db.query(VasPaymentQR).filter(VasPaymentQR.provider == provider_name).all()
  68. if not qrs:
  69. raise BizLogicError("No payment QR available")
  70. qr = random.choice(qrs)
  71. payment.qr_id = qr.id
  72. rate_key = f"{order.currency}->{provider.currency}".upper()
  73. exchange_rate = Decimal(rate_table[rate_key])
  74. converted = (
  75. Decimal(payment.base_amount) * exchange_rate
  76. ).quantize(Decimal("1"), rounding=ROUND_HALF_UP)
  77. max_discount = min(99, int(converted * Decimal("0.01")))
  78. discount = random.randint(1, max_discount) if max_discount >= 1 else 0
  79. final_amount = int(converted) - discount
  80. payment.exchange_rate = exchange_rate
  81. payment.amount = final_amount
  82. payment.currency = provider.currency
  83. payment.random_offset = discount
  84. return payment
  85. @staticmethod
  86. def create_stripe_payment(db, order, rate_table: dict):
  87. payment = PaymentService._create_stripe_payment(db, order)
  88. provider = db.query(VasPaymentProvider).filter(
  89. VasPaymentProvider.enabled == 1,
  90. VasPaymentProvider.name == 'stripe'
  91. ).first()
  92. rate_key = f"{order.currency}->{provider.currency}".upper()
  93. exchange_rate = Decimal(rate_table[rate_key])
  94. converted = (
  95. Decimal(payment.base_amount) * exchange_rate
  96. ).quantize(Decimal("1"), rounding=ROUND_HALF_UP)
  97. payment.exchange_rate = exchange_rate
  98. payment.amount = int(converted)
  99. payment.currency = provider.currency
  100. payment.random_offset = 0
  101. stripe_session = PaymentService.create_checkout_session(
  102. order=order,
  103. payment=payment,
  104. success_url="https://yourdomain.com/pay/success",
  105. cancel_url="https://yourdomain.com/pay/cancel"
  106. )
  107. payment.payment_intent_id = stripe_session.id
  108. payment.payment_url = stripe_session.url
  109. return payment
  110. @staticmethod
  111. def create_checkout_session(
  112. order,
  113. payment,
  114. success_url: str,
  115. cancel_url: str,
  116. ):
  117. """
  118. order.base_amount 单位:cent
  119. payment.amount 单位:cent
  120. """
  121. expires_at = int(time.time()) + 30 * 60 # Stripe 专用
  122. session = stripe.checkout.Session.create(
  123. mode="payment",
  124. payment_method_types=["card"],
  125. line_items=[
  126. {
  127. "price_data": {
  128. "currency": payment.currency.lower(),
  129. "product_data": {
  130. "name": f"Visa Service Order {order.id}",
  131. },
  132. "unit_amount": payment.amount,
  133. },
  134. "quantity": 1,
  135. }
  136. ],
  137. metadata={
  138. "order_id": order.id,
  139. "payment_id": payment.id,
  140. "user_id": order.user_id,
  141. },
  142. success_url=success_url + "?session_id={CHECKOUT_SESSION_ID}",
  143. cancel_url=cancel_url,
  144. expires_at=expires_at,
  145. )
  146. return session
  147. @staticmethod
  148. def _create_wechat_payment(db: Session, order: VasOrder):
  149. payment = VasPayment(
  150. order_id=order.id,
  151. provider="wechat",
  152. channel="qr_static",
  153. base_amount=order.base_amount,
  154. base_currency=order.currency,
  155. amount=0,
  156. currency="CNY",
  157. random_offset=0,
  158. exchange_rate=0,
  159. status="pending",
  160. expire_at=datetime.utcnow() + timedelta(minutes=30),
  161. )
  162. db.add(payment)
  163. db.flush()
  164. return payment
  165. @staticmethod
  166. def _create_alipay_payment(db: Session, order: VasOrder):
  167. payment = VasPayment(
  168. order_id=order.id,
  169. provider="alipay",
  170. channel="qr_static",
  171. base_amount=order.base_amount,
  172. base_currency=order.currency,
  173. amount=0,
  174. currency="CNY",
  175. random_offset=0,
  176. exchange_rate=0,
  177. status="pending",
  178. expire_at=datetime.utcnow() + timedelta(minutes=30),
  179. )
  180. db.add(payment)
  181. db.flush()
  182. return payment
  183. @staticmethod
  184. def _create_stripe_payment(db: Session, order: VasOrder):
  185. payment = VasPayment(
  186. order_id=order.id,
  187. provider="stripe",
  188. channel="online_link",
  189. base_amount=order.base_amount,
  190. base_currency=order.currency,
  191. amount=0,
  192. currency="EUR",
  193. random_offset=0,
  194. exchange_rate=0,
  195. status="pending",
  196. expire_at=datetime.utcnow() + timedelta(minutes=30),
  197. )
  198. db.add(payment)
  199. db.flush()
  200. return payment