会员积分改版V6 修复订单异步时区校验BUG
This commit is contained in:
@@ -217,11 +217,30 @@ async def wechat_callback(request: Request, db: AsyncSession = Depends(get_db)):
|
|||||||
total_amount = amount_info.get("total", 0) / 100 # 转换为元
|
total_amount = amount_info.get("total", 0) / 100 # 转换为元
|
||||||
|
|
||||||
if order_no:
|
if order_no:
|
||||||
await process_payment_success_by_order_no(db, order_no, transaction_id, total_amount)
|
try:
|
||||||
logger.info(
|
processed = await process_payment_success_by_order_no(
|
||||||
f"WeChat callback processed: order_no={order_no}, "
|
db, order_no, transaction_id, total_amount
|
||||||
f"transaction_id={transaction_id}, amount={total_amount}"
|
)
|
||||||
)
|
if processed:
|
||||||
|
logger.info(
|
||||||
|
f"WeChat callback processed: order_no={order_no}, "
|
||||||
|
f"transaction_id={transaction_id}, amount={total_amount}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logger.warning(
|
||||||
|
f"WeChat payment success not fulfilled locally: order_no={order_no}, "
|
||||||
|
f"transaction_id={transaction_id}, amount={total_amount}"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
# 微信已经通过验签、解密并明确通知支付成功。
|
||||||
|
# 本地履约异常属于项目内部故障:回滚本地事务,但仍向微信返回 SUCCESS,
|
||||||
|
# 后续由 pending 主动查单重试或日志人工对账处理。
|
||||||
|
await db.rollback()
|
||||||
|
logger.exception(
|
||||||
|
f"WeChat payment fulfillment error: order_no={order_no}, "
|
||||||
|
f"transaction_id={transaction_id}, error={e}"
|
||||||
|
)
|
||||||
|
return {"code": "SUCCESS", "message": "OK"}
|
||||||
|
|
||||||
# 处理退款回调
|
# 处理退款回调
|
||||||
elif event_type == "REFUND.SUCCESS":
|
elif event_type == "REFUND.SUCCESS":
|
||||||
@@ -402,19 +421,26 @@ async def cancel_order(
|
|||||||
if order.status != "pending":
|
if order.status != "pending":
|
||||||
raise HTTPException(status_code=400, detail=f"订单状态为{order.status},无法取消")
|
raise HTTPException(status_code=400, detail=f"订单状态为{order.status},无法取消")
|
||||||
|
|
||||||
# If it's an Alipay or WeChat order, call close API first
|
# Alipay/WeChat must confirm remote close before local cancellation.
|
||||||
|
# If close fails or the remote state is uncertain, keep the local order
|
||||||
|
# pending so a later gateway query can still discover a successful payment.
|
||||||
db_configs = await _get_payment_configs(db)
|
db_configs = await _get_payment_configs(db)
|
||||||
|
closed = True
|
||||||
if order.payment_method == "alipay":
|
if order.payment_method == "alipay":
|
||||||
try:
|
closed = await _close_alipay_order(db, order, db_configs)
|
||||||
await _close_alipay_order(db, order, db_configs)
|
|
||||||
except Exception as e:
|
|
||||||
logger.exception(f"Failed to close Alipay order {order_no}: {e}")
|
|
||||||
elif order.payment_method == "wechat":
|
elif order.payment_method == "wechat":
|
||||||
try:
|
from app.services.payment import _close_wechat_order
|
||||||
from app.services.payment import _close_wechat_order
|
closed = await _close_wechat_order(db, order, db_configs)
|
||||||
await _close_wechat_order(db, order, db_configs)
|
|
||||||
except Exception as e:
|
if not closed:
|
||||||
logger.exception(f"Failed to close WeChat order {order_no}: {e}")
|
logger.warning(
|
||||||
|
f"ORDER_CANCEL_CLOSE_PENDING order_no={order_no} "
|
||||||
|
f"user={current_user.id} method={order.payment_method}"
|
||||||
|
)
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=409,
|
||||||
|
detail="支付渠道暂未确认订单关闭,请稍后重试",
|
||||||
|
)
|
||||||
|
|
||||||
order.status = "cancelled"
|
order.status = "cancelled"
|
||||||
if order.upgrade_period_ids_json:
|
if order.upgrade_period_ids_json:
|
||||||
|
|||||||
@@ -170,46 +170,69 @@ async def _get_payment_configs(db: AsyncSession) -> dict[str, str]:
|
|||||||
return {c.key: c.value for c in result.scalars().all()}
|
return {c.key: c.value for c in result.scalars().all()}
|
||||||
|
|
||||||
|
|
||||||
|
async def _close_order_for_cancellation(
|
||||||
|
db: AsyncSession,
|
||||||
|
order: PaymentOrder,
|
||||||
|
db_configs: dict[str, str],
|
||||||
|
) -> bool:
|
||||||
|
"""Close a gateway order before changing the local order to cancelled.
|
||||||
|
|
||||||
|
For Alipay/WeChat, local cancellation is only allowed after the gateway
|
||||||
|
confirms that the order has been closed. Unknown/legacy payment methods
|
||||||
|
keep the historical local-only cancellation behaviour.
|
||||||
|
"""
|
||||||
|
if order.payment_method == "alipay":
|
||||||
|
return await _close_alipay_order(db, order, db_configs)
|
||||||
|
if order.payment_method == "wechat":
|
||||||
|
return await _close_wechat_order(db, order, db_configs)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def _check_and_expire_order(db: AsyncSession, order: PaymentOrder) -> bool:
|
async def _check_and_expire_order(db: AsyncSession, order: PaymentOrder) -> bool:
|
||||||
"""If a pending order has passed its expiry, mark it cancelled.
|
"""Expire a pending order only after the payment gateway confirms close.
|
||||||
Returns True if the order was expired.
|
|
||||||
|
Returns True only when the local order was actually changed to cancelled.
|
||||||
|
If the gateway close result is uncertain/failed, the order remains pending
|
||||||
|
so that later query/reconciliation can still discover a successful payment.
|
||||||
"""
|
"""
|
||||||
if order.status != "pending":
|
if order.status != "pending":
|
||||||
return False
|
return False
|
||||||
|
|
||||||
db_configs = await _get_payment_configs(db)
|
db_configs = await _get_payment_configs(db)
|
||||||
expire_seconds = _get_order_expire_seconds(db_configs)
|
expire_seconds = _get_order_expire_seconds(db_configs)
|
||||||
expiry = order.created_at + timedelta(seconds=expire_seconds)
|
expiry = order.created_at + timedelta(seconds=expire_seconds)
|
||||||
if datetime.now(order.created_at.tzinfo) >= expiry:
|
if utc_now() < expiry:
|
||||||
order.status = "cancelled"
|
return False
|
||||||
if order.upgrade_period_ids_json:
|
|
||||||
await release_upgrade_reservation(db, order=order, released_at=utc_now())
|
closed = await _close_order_for_cancellation(db, order, db_configs)
|
||||||
await db.flush()
|
if not closed:
|
||||||
logger.info(
|
logger.warning(
|
||||||
f"ORDER_EXPIRED order_no={order.order_no} user={order.user_id} "
|
f"ORDER_EXPIRE_CLOSE_PENDING order_no={order.order_no} "
|
||||||
f"amount={order.amount} created_at={order.created_at.isoformat()}"
|
f"user={order.user_id} amount={order.amount}"
|
||||||
)
|
)
|
||||||
# Also call close API if it was an Alipay or WeChat order
|
return False
|
||||||
if order.payment_method == "alipay":
|
|
||||||
try:
|
order.status = "cancelled"
|
||||||
await _close_alipay_order(db, order, db_configs)
|
if order.upgrade_period_ids_json:
|
||||||
except Exception as e:
|
await release_upgrade_reservation(db, order=order, released_at=utc_now())
|
||||||
logger.exception(f"Failed to close Alipay order {order.order_no}: {e}")
|
await db.flush()
|
||||||
elif order.payment_method == "wechat":
|
logger.info(
|
||||||
try:
|
f"ORDER_EXPIRED order_no={order.order_no} user={order.user_id} "
|
||||||
await _close_wechat_order(db, order, db_configs)
|
f"amount={order.amount} created_at={order.created_at.isoformat()}"
|
||||||
except Exception as e:
|
)
|
||||||
logger.exception(f"Failed to close WeChat order {order.order_no}: {e}")
|
return True
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
async def expire_all_pending_orders(db: AsyncSession) -> int:
|
async def expire_all_pending_orders(db: AsyncSession) -> int:
|
||||||
"""Background task: mark all expired pending orders as cancelled.
|
"""Background task: close and cancel expired pending orders.
|
||||||
Returns the number of orders expired.
|
|
||||||
|
Alipay/WeChat orders remain pending when the remote close cannot be
|
||||||
|
confirmed. This prevents a real paid order from being persisted locally
|
||||||
|
as cancelled.
|
||||||
"""
|
"""
|
||||||
db_configs = await _get_payment_configs(db)
|
db_configs = await _get_payment_configs(db)
|
||||||
expire_seconds = _get_order_expire_seconds(db_configs)
|
expire_seconds = _get_order_expire_seconds(db_configs)
|
||||||
threshold = datetime.now() - timedelta(seconds=expire_seconds)
|
threshold = utc_now() - timedelta(seconds=expire_seconds)
|
||||||
result = await db.execute(
|
result = await db.execute(
|
||||||
select(PaymentOrder).where(
|
select(PaymentOrder).where(
|
||||||
PaymentOrder.status == "pending",
|
PaymentOrder.status == "pending",
|
||||||
@@ -218,27 +241,27 @@ async def expire_all_pending_orders(db: AsyncSession) -> int:
|
|||||||
)
|
)
|
||||||
orders = result.scalars().all()
|
orders = result.scalars().all()
|
||||||
expired_count = 0
|
expired_count = 0
|
||||||
for o in orders:
|
|
||||||
o.status = "cancelled"
|
for order in orders:
|
||||||
if o.upgrade_period_ids_json:
|
closed = await _close_order_for_cancellation(db, order, db_configs)
|
||||||
await release_upgrade_reservation(db, order=o, released_at=utc_now())
|
if not closed:
|
||||||
|
logger.warning(
|
||||||
|
f"ORDER_EXPIRE_CLOSE_PENDING order_no={order.order_no} "
|
||||||
|
f"user={order.user_id} amount={order.amount}"
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
order.status = "cancelled"
|
||||||
|
if order.upgrade_period_ids_json:
|
||||||
|
await release_upgrade_reservation(db, order=order, released_at=utc_now())
|
||||||
expired_count += 1
|
expired_count += 1
|
||||||
logger.info(
|
logger.info(
|
||||||
f"ORDER_EXPIRED order_no={o.order_no} user={o.user_id} amount={o.amount}"
|
f"ORDER_EXPIRED order_no={order.order_no} user={order.user_id} "
|
||||||
|
f"amount={order.amount}"
|
||||||
)
|
)
|
||||||
# Also call close API if it was an Alipay or WeChat order
|
|
||||||
if o.payment_method == "alipay":
|
if expired_count > 0:
|
||||||
try:
|
await db.commit()
|
||||||
await _close_alipay_order(db, o, db_configs)
|
|
||||||
except Exception as e:
|
|
||||||
logger.exception(f"Failed to close Alipay order {o.order_no}: {e}")
|
|
||||||
elif o.payment_method == "wechat":
|
|
||||||
try:
|
|
||||||
await _close_wechat_order(db, o, db_configs)
|
|
||||||
except Exception as e:
|
|
||||||
logger.exception(f"Failed to close WeChat order {o.order_no}: {e}")
|
|
||||||
if orders:
|
|
||||||
await db.flush()
|
|
||||||
return expired_count
|
return expired_count
|
||||||
|
|
||||||
|
|
||||||
@@ -409,19 +432,15 @@ async def create_recharge_order(
|
|||||||
if mock_mode:
|
if mock_mode:
|
||||||
order.status = "paid"
|
order.status = "paid"
|
||||||
order.paid_at = checked_at
|
order.paid_at = checked_at
|
||||||
if product:
|
desc = f"充值{label}({total_credits}积分)"
|
||||||
await fulfill_payment_product(db, order=order, fulfilled_at=checked_at)
|
if bonus_credits > 0:
|
||||||
else:
|
desc += f"(含赠送{bonus_credits}积分)"
|
||||||
desc = f"充值{label}({total_credits}积分)"
|
await _fulfill_paid_order(
|
||||||
if bonus_credits > 0:
|
db,
|
||||||
desc += f"(含赠送{bonus_credits}积分)"
|
order=order,
|
||||||
await add_credits(
|
fulfilled_at=checked_at,
|
||||||
db, user_id, total_credits, desc, related_id=order.id,
|
legacy_description=desc,
|
||||||
biz_key=_payment_biz_key(order, charge_kind=CreditRecordChargeKind.RECHARGE.value, action=CreditRecordAction.CHARGE.value),
|
)
|
||||||
record_meta=_payment_recharge_meta(order),
|
|
||||||
payment_order_id=order.id,
|
|
||||||
source_id=order.id,
|
|
||||||
)
|
|
||||||
await db.flush()
|
await db.flush()
|
||||||
else:
|
else:
|
||||||
if method == "wechat":
|
if method == "wechat":
|
||||||
@@ -953,60 +972,78 @@ async def _query_alipay_order(db: AsyncSession, order: PaymentOrder, db_configs:
|
|||||||
|
|
||||||
async def sync_pending_orders(db: AsyncSession) -> int:
|
async def sync_pending_orders(db: AsyncSession) -> int:
|
||||||
"""Check pending orders via Alipay/WeChat query and update status.
|
"""Check pending orders via Alipay/WeChat query and update status.
|
||||||
Returns the number of orders updated.
|
|
||||||
|
Candidate rows are collected as primitive values first. A failed payment
|
||||||
|
fulfillment may roll back the session; using primitive candidates prevents
|
||||||
|
that rollback from expiring ORM objects needed by later iterations.
|
||||||
"""
|
"""
|
||||||
result = await db.execute(
|
result = await db.execute(
|
||||||
select(PaymentOrder).where(
|
select(PaymentOrder.order_no, PaymentOrder.payment_method).where(
|
||||||
PaymentOrder.status == "pending",
|
PaymentOrder.status == "pending",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
orders = result.scalars().all()
|
candidates = [(str(row.order_no), str(row.payment_method)) for row in result.all()]
|
||||||
updated_count = 0
|
updated_count = 0
|
||||||
|
|
||||||
db_configs = await _get_payment_configs(db)
|
db_configs = await _get_payment_configs(db)
|
||||||
|
|
||||||
for order in orders:
|
for order_no, payment_method in candidates:
|
||||||
try:
|
try:
|
||||||
if order.payment_method == "alipay":
|
order_result = await db.execute(
|
||||||
|
select(PaymentOrder).where(
|
||||||
|
PaymentOrder.order_no == order_no,
|
||||||
|
PaymentOrder.status == "pending",
|
||||||
|
).limit(1)
|
||||||
|
)
|
||||||
|
order = order_result.scalar_one_or_none()
|
||||||
|
if order is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if payment_method == "alipay":
|
||||||
data = await _query_alipay_order(db, order, db_configs)
|
data = await _query_alipay_order(db, order, db_configs)
|
||||||
if data:
|
if data:
|
||||||
trade_status = data.get("trade_status")
|
trade_status = data.get("trade_status")
|
||||||
if trade_status in ("TRADE_SUCCESS", "TRADE_FINISHED"):
|
if trade_status in ("TRADE_SUCCESS", "TRADE_FINISHED"):
|
||||||
# Order was paid but we missed the callback
|
|
||||||
trade_no = data.get("trade_no", "")
|
trade_no = data.get("trade_no", "")
|
||||||
total_amount_str = data.get("total_amount", "")
|
total_amount_str = data.get("total_amount", "")
|
||||||
total_amount = float(total_amount_str) if total_amount_str else None
|
total_amount = float(total_amount_str) if total_amount_str else None
|
||||||
await process_payment_success_by_order_no(db, order.order_no, trade_no, total_amount)
|
processed = await process_payment_success_by_order_no(
|
||||||
updated_count += 1
|
db, order_no, trade_no, total_amount
|
||||||
|
)
|
||||||
|
if processed:
|
||||||
|
updated_count += 1
|
||||||
elif trade_status in ("TRADE_CLOSED", "TRADE_CANCELLED"):
|
elif trade_status in ("TRADE_CLOSED", "TRADE_CANCELLED"):
|
||||||
# Order was closed on Alipay side
|
|
||||||
order.status = "cancelled"
|
order.status = "cancelled"
|
||||||
if order.upgrade_period_ids_json:
|
if order.upgrade_period_ids_json:
|
||||||
await release_upgrade_reservation(db, order=order, released_at=utc_now())
|
await release_upgrade_reservation(
|
||||||
await db.flush()
|
db, order=order, released_at=utc_now()
|
||||||
|
)
|
||||||
|
await db.commit()
|
||||||
updated_count += 1
|
updated_count += 1
|
||||||
elif order.payment_method == "wechat":
|
|
||||||
|
elif payment_method == "wechat":
|
||||||
data = await _query_wechat_order(db, order, db_configs)
|
data = await _query_wechat_order(db, order, db_configs)
|
||||||
if data:
|
if data:
|
||||||
trade_state = data.get("trade_state")
|
trade_state = data.get("trade_state")
|
||||||
if trade_state == "SUCCESS":
|
if trade_state == "SUCCESS":
|
||||||
# Order was paid but we missed the callback
|
|
||||||
transaction_id = data.get("transaction_id", "")
|
transaction_id = data.get("transaction_id", "")
|
||||||
total_amount = float(data.get("amount", {}).get("total", 0)) / 100
|
total_amount = float(data.get("amount", {}).get("total", 0)) / 100
|
||||||
await process_payment_success_by_order_no(db, order.order_no, transaction_id, total_amount)
|
processed = await process_payment_success_by_order_no(
|
||||||
updated_count += 1
|
db, order_no, transaction_id, total_amount
|
||||||
|
)
|
||||||
|
if processed:
|
||||||
|
updated_count += 1
|
||||||
elif trade_state in ("CLOSED", "REVOKED"):
|
elif trade_state in ("CLOSED", "REVOKED"):
|
||||||
# Order was closed on WeChat side
|
|
||||||
order.status = "cancelled"
|
order.status = "cancelled"
|
||||||
if order.upgrade_period_ids_json:
|
if order.upgrade_period_ids_json:
|
||||||
await release_upgrade_reservation(db, order=order, released_at=utc_now())
|
await release_upgrade_reservation(
|
||||||
await db.flush()
|
db, order=order, released_at=utc_now()
|
||||||
|
)
|
||||||
|
await db.commit()
|
||||||
updated_count += 1
|
updated_count += 1
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception(f"Failed to sync order {order.order_no}: {e}")
|
await db.rollback()
|
||||||
|
logger.exception(f"Failed to sync order {order_no}: {e}")
|
||||||
|
|
||||||
if updated_count > 0:
|
|
||||||
await db.flush()
|
|
||||||
return updated_count
|
return updated_count
|
||||||
|
|
||||||
|
|
||||||
@@ -1179,96 +1216,152 @@ async def verify_wechat_callback(data: dict, db: AsyncSession) -> bool:
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
async def process_payment_success(db: AsyncSession, order_id: str):
|
async def _fulfill_paid_order(
|
||||||
"""Process successful payment: update order and add credits."""
|
db: AsyncSession,
|
||||||
result = await db.execute(
|
*,
|
||||||
select(PaymentOrder).where(PaymentOrder.id == order_id).with_for_update().limit(1)
|
order: PaymentOrder,
|
||||||
)
|
fulfilled_at: datetime,
|
||||||
order = result.scalar_one_or_none()
|
legacy_description: str,
|
||||||
if not order or order.status != "pending":
|
) -> None:
|
||||||
|
"""Fulfill the business side of a paid order without committing.
|
||||||
|
|
||||||
|
Product orders are considered successfully paid locally only when the
|
||||||
|
product service leaves the order in the explicit ``fulfilled`` state.
|
||||||
|
This turns any internal upgrade/reconciliation failure into an exception so
|
||||||
|
the outer payment transaction can roll back the temporary ``paid`` state.
|
||||||
|
"""
|
||||||
|
if order.product_id:
|
||||||
|
await fulfill_payment_product(db, order=order, fulfilled_at=fulfilled_at)
|
||||||
|
if order.fulfillment_status != "fulfilled":
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Payment product fulfillment incomplete: order_no={order.order_no}, "
|
||||||
|
f"fulfillment_status={order.fulfillment_status}"
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
order.status = "paid"
|
await add_credits(
|
||||||
order.paid_at = datetime.now()
|
db,
|
||||||
if order.product_id:
|
order.user_id,
|
||||||
await fulfill_payment_product(db, order=order, fulfilled_at=order.paid_at)
|
order.credits,
|
||||||
else:
|
legacy_description,
|
||||||
await add_credits(
|
related_id=order.id,
|
||||||
db, order.user_id, order.credits, f"充值成功({order.credits}积分)",
|
biz_key=_payment_biz_key(
|
||||||
related_id=order.id,
|
order,
|
||||||
biz_key=_payment_biz_key(order, charge_kind=CreditRecordChargeKind.RECHARGE.value, action=CreditRecordAction.CHARGE.value),
|
charge_kind=CreditRecordChargeKind.RECHARGE.value,
|
||||||
record_meta=_payment_recharge_meta(order),
|
action=CreditRecordAction.CHARGE.value,
|
||||||
payment_order_id=order.id,
|
),
|
||||||
source_id=order.id,
|
record_meta=_payment_recharge_meta(order),
|
||||||
|
payment_order_id=order.id,
|
||||||
|
source_id=order.id,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def process_payment_success(db: AsyncSession, order_id: str) -> bool:
|
||||||
|
"""Atomically mark a pending order paid and fulfill its business value."""
|
||||||
|
try:
|
||||||
|
result = await db.execute(
|
||||||
|
select(PaymentOrder)
|
||||||
|
.where(PaymentOrder.id == order_id)
|
||||||
|
.with_for_update()
|
||||||
|
.limit(1)
|
||||||
)
|
)
|
||||||
await db.commit()
|
order = result.scalar_one_or_none()
|
||||||
|
if not order:
|
||||||
|
return False
|
||||||
|
if order.status == "paid":
|
||||||
|
return True
|
||||||
|
if order.status != "pending":
|
||||||
|
return False
|
||||||
|
|
||||||
|
paid_at = utc_now()
|
||||||
|
order.status = "paid"
|
||||||
|
order.paid_at = paid_at
|
||||||
|
await _fulfill_paid_order(
|
||||||
|
db,
|
||||||
|
order=order,
|
||||||
|
fulfilled_at=paid_at,
|
||||||
|
legacy_description=f"充值成功({order.credits}积分)",
|
||||||
|
)
|
||||||
|
await db.commit()
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
await db.rollback()
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
async def process_payment_success_by_order_no(
|
async def process_payment_success_by_order_no(
|
||||||
db: AsyncSession,
|
db: AsyncSession,
|
||||||
order_no: str,
|
order_no: str,
|
||||||
trade_no: str = "",
|
trade_no: str = "",
|
||||||
total_amount: float | None = None
|
total_amount: float | None = None,
|
||||||
):
|
) -> bool:
|
||||||
"""Process successful payment by order_no (used by Alipay/WeChat callbacks).
|
"""Atomically process an Alipay/WeChat payment success.
|
||||||
|
|
||||||
Args:
|
``paid`` is persisted only together with successful local fulfillment.
|
||||||
db: async database session
|
When fulfillment fails, the whole local transaction is rolled back and the
|
||||||
order_no: the merchant order number (out_trade_no)
|
order remains ``pending``; the existing gateway query loop may retry it.
|
||||||
trade_no: the Alipay trade number (trade_no), optional
|
|
||||||
total_amount: the payment amount from the gateway, for consistency check
|
|
||||||
"""
|
"""
|
||||||
result = await db.execute(
|
try:
|
||||||
select(PaymentOrder).where(PaymentOrder.order_no == order_no).with_for_update().limit(1)
|
result = await db.execute(
|
||||||
)
|
select(PaymentOrder)
|
||||||
order = result.scalar_one_or_none()
|
.where(PaymentOrder.order_no == order_no)
|
||||||
|
.with_for_update()
|
||||||
if not order:
|
.limit(1)
|
||||||
logger.info(f"Order {order_no} not found, skipping")
|
|
||||||
return
|
|
||||||
|
|
||||||
if order.status == "paid":
|
|
||||||
logger.info(f"Order {order_no} already processed, skipping")
|
|
||||||
return
|
|
||||||
|
|
||||||
if order.status != "pending":
|
|
||||||
logger.info(f"Order {order_no} is in {order.status} state, cannot process")
|
|
||||||
return
|
|
||||||
|
|
||||||
# 金额一致性校验
|
|
||||||
if total_amount is not None and abs(to_credit_decimal(total_amount) - to_credit_decimal(order.amount)) > to_credit_decimal("0.01"):
|
|
||||||
logger.error(
|
|
||||||
f"Amount mismatch: order amount {order.amount}, gateway amount {total_amount}"
|
|
||||||
)
|
)
|
||||||
return
|
order = result.scalar_one_or_none()
|
||||||
|
|
||||||
# 幂等性检查:如果trade_no已存在且相同,则跳过
|
if not order:
|
||||||
if trade_no and order.trade_no and order.trade_no == trade_no:
|
logger.info(f"Order {order_no} not found, skipping")
|
||||||
logger.info(f"Trade no {trade_no} already processed, skipping")
|
return False
|
||||||
return
|
|
||||||
|
|
||||||
order.status = "paid"
|
if order.status == "paid":
|
||||||
order.paid_at = datetime.now()
|
logger.info(f"Order {order_no} already processed, skipping")
|
||||||
if trade_no:
|
return True
|
||||||
order.trade_no = trade_no
|
|
||||||
|
|
||||||
if order.product_id:
|
if order.status != "pending":
|
||||||
await fulfill_payment_product(db, order=order, fulfilled_at=order.paid_at)
|
logger.info(f"Order {order_no} is in {order.status} state, cannot process")
|
||||||
else:
|
return False
|
||||||
await add_credits(
|
|
||||||
db, order.user_id, order.credits,
|
if (
|
||||||
f"充值成功({order.credits}积分), 订单号: {order_no}, 金额: {order.amount}",
|
total_amount is not None
|
||||||
related_id=order.id,
|
and abs(
|
||||||
biz_key=_payment_biz_key(order, charge_kind=CreditRecordChargeKind.RECHARGE.value, action=CreditRecordAction.CHARGE.value),
|
to_credit_decimal(total_amount) - to_credit_decimal(order.amount)
|
||||||
record_meta=_payment_recharge_meta(order),
|
) > to_credit_decimal("0.01")
|
||||||
payment_order_id=order.id,
|
):
|
||||||
source_id=order.id,
|
logger.error(
|
||||||
|
f"Amount mismatch: order amount {order.amount}, gateway amount {total_amount}"
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
# A pending order must never be skipped merely because trade_no is
|
||||||
|
# already present. Historical partial writes can otherwise remain
|
||||||
|
# pending forever. The row lock + paid state + credit biz_key provide
|
||||||
|
# the actual idempotency boundary.
|
||||||
|
paid_at = utc_now()
|
||||||
|
order.status = "paid"
|
||||||
|
order.paid_at = paid_at
|
||||||
|
if trade_no:
|
||||||
|
order.trade_no = trade_no
|
||||||
|
|
||||||
|
await _fulfill_paid_order(
|
||||||
|
db,
|
||||||
|
order=order,
|
||||||
|
fulfilled_at=paid_at,
|
||||||
|
legacy_description=(
|
||||||
|
f"充值成功({order.credits}积分), "
|
||||||
|
f"订单号: {order_no}, 金额: {order.amount}"
|
||||||
|
),
|
||||||
)
|
)
|
||||||
await db.commit()
|
|
||||||
logger.info(
|
await db.commit()
|
||||||
f"PAYMENT_SUCCESS order_no={order_no} user={order.user_id} "
|
logger.info(
|
||||||
f"amount={order.amount} credits={order.credits} trade_no={trade_no}"
|
f"PAYMENT_SUCCESS order_no={order_no} user={order.user_id} "
|
||||||
)
|
f"amount={order.amount} credits={order.credits} trade_no={trade_no}"
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
await db.rollback()
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
async def process_refund(
|
async def process_refund(
|
||||||
@@ -1345,7 +1438,7 @@ async def process_refund(
|
|||||||
# 更新订单状态
|
# 更新订单状态
|
||||||
order.status = "refunded"
|
order.status = "refunded"
|
||||||
order.refund_amount = refund_amount
|
order.refund_amount = refund_amount
|
||||||
order.refunded_at = datetime.now()
|
order.refunded_at = utc_now()
|
||||||
if order.payment_method == "alipay":
|
if order.payment_method == "alipay":
|
||||||
order.refund_trade_no = db_configs.get("refund_trade_no", "")
|
order.refund_trade_no = db_configs.get("refund_trade_no", "")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user