1
This commit is contained in:
@@ -16,6 +16,9 @@ from app.services.payment import (
|
|||||||
verify_wechat_callback,
|
verify_wechat_callback,
|
||||||
verify_alipay_callback,
|
verify_alipay_callback,
|
||||||
process_payment_success_by_order_no,
|
process_payment_success_by_order_no,
|
||||||
|
_get_payment_configs,
|
||||||
|
_is_mock_mode,
|
||||||
|
_check_and_expire_order,
|
||||||
)
|
)
|
||||||
|
|
||||||
router = APIRouter(prefix="/payments", tags=["payments"])
|
router = APIRouter(prefix="/payments", tags=["payments"])
|
||||||
@@ -42,7 +45,6 @@ async def recharge(
|
|||||||
raise HTTPException(status_code=400, detail="不支持的支付方式")
|
raise HTTPException(status_code=400, detail="不支持的支付方式")
|
||||||
|
|
||||||
# Check if the selected payment method is enabled in admin config
|
# Check if the selected payment method is enabled in admin config
|
||||||
from app.services.payment import _get_payment_configs, _is_mock_mode
|
|
||||||
configs = await _get_payment_configs(db)
|
configs = await _get_payment_configs(db)
|
||||||
if not _is_mock_mode(configs):
|
if not _is_mock_mode(configs):
|
||||||
enabled_key = f"payment_{req.method}_enabled"
|
enabled_key = f"payment_{req.method}_enabled"
|
||||||
@@ -118,7 +120,6 @@ async def list_orders(
|
|||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
):
|
):
|
||||||
# Auto-expire stale pending orders before returning
|
# Auto-expire stale pending orders before returning
|
||||||
from app.services.payment import _check_and_expire_order
|
|
||||||
result = await db.execute(
|
result = await db.execute(
|
||||||
select(PaymentOrder)
|
select(PaymentOrder)
|
||||||
.where(PaymentOrder.user_id == current_user.id)
|
.where(PaymentOrder.user_id == current_user.id)
|
||||||
|
|||||||
@@ -23,6 +23,19 @@ from app.models.system_config import SystemConfig
|
|||||||
from app.services.credits import add_credits
|
from app.services.credits import add_credits
|
||||||
from app.utils.id_gen import generate_id, generate_order_no
|
from app.utils.id_gen import generate_id, generate_order_no
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"create_recharge_order",
|
||||||
|
"verify_alipay_callback",
|
||||||
|
"verify_wechat_callback",
|
||||||
|
"process_payment_success_by_order_no",
|
||||||
|
"get_order",
|
||||||
|
"get_user_orders",
|
||||||
|
"expire_all_pending_orders",
|
||||||
|
"_get_payment_configs",
|
||||||
|
"_is_mock_mode",
|
||||||
|
"_check_and_expire_order",
|
||||||
|
]
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Payment logger → log/payment/YYYY-MM-DD.log (one file per day, no cleanup)
|
# Payment logger → log/payment/YYYY-MM-DD.log (one file per day, no cleanup)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -135,6 +148,10 @@ 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()}
|
||||||
|
|
||||||
|
|
||||||
|
def _is_mock_mode(configs: dict[str, str]) -> bool:
|
||||||
|
return configs.get("payment_mock", "false").lower() == "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.
|
"""If a pending order has passed its expiry, mark it cancelled.
|
||||||
Returns True if the order was expired.
|
Returns True if the order was expired.
|
||||||
@@ -237,24 +254,27 @@ def _get_alipay_client(app_id: str, private_key: str, public_key: str, gateway:
|
|||||||
async def create_recharge_order(
|
async def create_recharge_order(
|
||||||
db: AsyncSession,
|
db: AsyncSession,
|
||||||
user_id: str,
|
user_id: str,
|
||||||
amount: float,
|
|
||||||
credits: float,
|
credits: float,
|
||||||
payment_method: str,
|
price: float,
|
||||||
|
label: str,
|
||||||
|
bonus_credits: float = 0,
|
||||||
|
method: str = "alipay",
|
||||||
) -> PaymentOrder:
|
) -> PaymentOrder:
|
||||||
"""Create a new pending payment order and call the payment gateway.
|
"""Create a new pending payment order and call the payment gateway.
|
||||||
If mock mode is enabled, auto-approves.
|
If mock mode is enabled, auto-approves.
|
||||||
Returns the PaymentOrder with qr_code (or None if mock).
|
Returns the PaymentOrder with qr_code (or None if mock).
|
||||||
"""
|
"""
|
||||||
configs = await _get_payment_configs(db)
|
configs = await _get_payment_configs(db)
|
||||||
is_mock = configs.get("payment_mock", "false").lower() == "true"
|
is_mock = _is_mock_mode(configs)
|
||||||
|
total_credits = credits + bonus_credits
|
||||||
|
|
||||||
order = PaymentOrder(
|
order = PaymentOrder(
|
||||||
id=generate_id(),
|
id=generate_id(),
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
order_no=generate_order_no(),
|
order_no=generate_order_no(),
|
||||||
amount=amount,
|
amount=price,
|
||||||
credits=credits,
|
credits=total_credits,
|
||||||
payment_method=payment_method,
|
payment_method=method,
|
||||||
status="pending" if not is_mock else "paid",
|
status="pending" if not is_mock else "paid",
|
||||||
qr_url=None,
|
qr_url=None,
|
||||||
)
|
)
|
||||||
@@ -263,7 +283,7 @@ async def create_recharge_order(
|
|||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f"ORDER_CREATED order_no={order.order_no} user={user_id} "
|
f"ORDER_CREATED order_no={order.order_no} user={user_id} "
|
||||||
f"amount={amount} credits={credits} method={payment_method} mock={is_mock}"
|
f"amount={price} credits={total_credits} method={method} mock={is_mock}"
|
||||||
)
|
)
|
||||||
|
|
||||||
if is_mock:
|
if is_mock:
|
||||||
@@ -273,12 +293,12 @@ async def create_recharge_order(
|
|||||||
return order
|
return order
|
||||||
|
|
||||||
# Real payment
|
# Real payment
|
||||||
if payment_method == "alipay":
|
if method == "alipay":
|
||||||
qr_url = _create_alipay_order(order, configs)
|
qr_url = _create_alipay_order(order, configs)
|
||||||
if qr_url:
|
if qr_url:
|
||||||
order.qr_url = qr_url
|
order.qr_url = qr_url
|
||||||
await db.flush()
|
await db.flush()
|
||||||
elif payment_method == "wechat":
|
elif method == "wechat":
|
||||||
_create_wechat_order(order, configs)
|
_create_wechat_order(order, configs)
|
||||||
# Wechat would get a qr_url too, but stubbed for now
|
# Wechat would get a qr_url too, but stubbed for now
|
||||||
|
|
||||||
@@ -286,6 +306,29 @@ async def create_recharge_order(
|
|||||||
return order
|
return order
|
||||||
|
|
||||||
|
|
||||||
|
async def verify_wechat_callback(data: dict, db: AsyncSession) -> bool:
|
||||||
|
"""Verify Wechat Pay callback signature.
|
||||||
|
Note: This is a stub implementation.
|
||||||
|
"""
|
||||||
|
configs = await _get_payment_configs(db)
|
||||||
|
wechat_api_key = configs.get("payment_wechat_api_key", "")
|
||||||
|
|
||||||
|
if not wechat_api_key:
|
||||||
|
logger.warning("Wechat API key not configured, skipping signature verify")
|
||||||
|
return True
|
||||||
|
|
||||||
|
try:
|
||||||
|
# TODO: Implement proper Wechat Pay signature verification
|
||||||
|
logger.info(
|
||||||
|
f"WECHAT_CALLBACK order_no={data.get('out_trade_no')} "
|
||||||
|
f"transaction_id={data.get('transaction_id')}"
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Error verifying Wechat callback")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Wechat – stub for now
|
# Wechat – stub for now
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user