payment_service.py 7.5 KB

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