Merge branch 'main' of https://gitee.com/wg123/video-gen
This commit is contained in:
@@ -43,6 +43,7 @@ from app.services.notification import create_notification
|
||||
from app.services.auth import hash_password, verify_password
|
||||
from app.services.operation_log import log_operation
|
||||
from app.services.resource_signed_url_service import build_resource_signed_url
|
||||
from app.services.payment import sync_pending_orders, process_refund
|
||||
|
||||
from app.services.generation_billing_service import (
|
||||
OWNER_GENERATION_RECORD,
|
||||
@@ -442,26 +443,70 @@ async def batch_update_payment_configs(
|
||||
|
||||
@router.get("/payment-stats")
|
||||
async def get_payment_stats(
|
||||
payment_method: str | None = Query(None),
|
||||
status: str | None = Query(None),
|
||||
start_date: str | None = Query(None),
|
||||
end_date: str | None = Query(None),
|
||||
admin: User = Depends(get_admin_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Return payment statistics for admin dashboard."""
|
||||
"""Return payment statistics for admin dashboard with filters."""
|
||||
from sqlalchemy import func
|
||||
|
||||
# Ensure by_status has all expected statuses with defaults
|
||||
by_status = {
|
||||
"pending": {"count": 0, "amount": 0.0},
|
||||
"paid": {"count": 0, "amount": 0.0},
|
||||
"cancelled": {"count": 0, "amount": 0.0},
|
||||
"refunded": {"count": 0, "amount": 0.0},
|
||||
}
|
||||
|
||||
# Parse dates and build base query filters
|
||||
now_cst = datetime.now(CST)
|
||||
today_start = now_cst.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
today_end = today_start + timedelta(days=1)
|
||||
|
||||
# Default to today if no date range provided
|
||||
query_start = today_start
|
||||
query_end = today_end
|
||||
|
||||
if start_date:
|
||||
query_start = datetime.fromisoformat(start_date).replace(tzinfo=CST)
|
||||
if end_date:
|
||||
query_end = (datetime.fromisoformat(end_date) + timedelta(days=1)).replace(tzinfo=CST)
|
||||
|
||||
# Build filter list for status breakdown
|
||||
breakdown_filters = []
|
||||
if payment_method:
|
||||
breakdown_filters.append(PaymentOrder.payment_method == payment_method)
|
||||
if status:
|
||||
breakdown_filters.append(PaymentOrder.status == status)
|
||||
# Always apply date range to breakdown
|
||||
breakdown_filters.append(PaymentOrder.created_at >= query_start)
|
||||
breakdown_filters.append(PaymentOrder.created_at < query_end)
|
||||
|
||||
# Status breakdown
|
||||
status_result = await db.execute(
|
||||
select(
|
||||
PaymentOrder.status,
|
||||
func.count().label("count"),
|
||||
func.coalesce(func.sum(PaymentOrder.amount), 0).label("amount"),
|
||||
).group_by(PaymentOrder.status)
|
||||
)
|
||||
.where(*breakdown_filters)
|
||||
.group_by(PaymentOrder.status)
|
||||
)
|
||||
by_status = {}
|
||||
for row in status_result.all():
|
||||
by_status[row.status] = {"count": row.count, "amount": float(row.amount)}
|
||||
if row.status in by_status:
|
||||
by_status[row.status] = {
|
||||
"count": row.count,
|
||||
"amount": round(float(row.amount), 2)
|
||||
}
|
||||
else:
|
||||
# Map any unexpected status to cancelled
|
||||
by_status["cancelled"]["count"] += row.count
|
||||
by_status["cancelled"]["amount"] += round(float(row.amount), 2)
|
||||
|
||||
# Today's stats
|
||||
today_start = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
# Today's stats (CST time zone) - independent of filter
|
||||
today_result = await db.execute(
|
||||
select(
|
||||
func.count().label("paid_count"),
|
||||
@@ -469,38 +514,70 @@ async def get_payment_stats(
|
||||
).where(
|
||||
PaymentOrder.status == "paid",
|
||||
PaymentOrder.paid_at >= today_start,
|
||||
PaymentOrder.paid_at < today_end,
|
||||
)
|
||||
)
|
||||
today_row = today_result.one()
|
||||
|
||||
# Recent 50 orders
|
||||
# Monthly cumulative stats
|
||||
month_start = now_cst.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
|
||||
month_end = (month_start + timedelta(days=32)).replace(day=1, hour=0, minute=0, second=0, microsecond=0)
|
||||
|
||||
month_result = await db.execute(
|
||||
select(
|
||||
func.count().label("paid_count"),
|
||||
func.coalesce(func.sum(PaymentOrder.amount), 0).label("paid_amount"),
|
||||
).where(
|
||||
PaymentOrder.status == "paid",
|
||||
PaymentOrder.paid_at >= month_start,
|
||||
PaymentOrder.paid_at < month_end,
|
||||
)
|
||||
)
|
||||
month_row = month_result.one()
|
||||
|
||||
# Recent orders with filters
|
||||
recent_filters = []
|
||||
if payment_method:
|
||||
recent_filters.append(PaymentOrder.payment_method == payment_method)
|
||||
if status:
|
||||
recent_filters.append(PaymentOrder.status == status)
|
||||
recent_filters.append(PaymentOrder.created_at >= query_start)
|
||||
recent_filters.append(PaymentOrder.created_at < query_end)
|
||||
|
||||
recent_result = await db.execute(
|
||||
select(PaymentOrder)
|
||||
select(PaymentOrder, User)
|
||||
.join(User, PaymentOrder.user_id == User.id)
|
||||
.where(*recent_filters)
|
||||
.order_by(PaymentOrder.created_at.desc())
|
||||
.limit(50)
|
||||
)
|
||||
recent = recent_result.scalars().all()
|
||||
recent_data = recent_result.all()
|
||||
|
||||
return {
|
||||
"by_status": by_status,
|
||||
"today": {
|
||||
"paid_count": today_row.paid_count,
|
||||
"paid_amount": float(today_row.paid_amount),
|
||||
"paid_amount": round(float(today_row.paid_amount), 2),
|
||||
},
|
||||
"month": {
|
||||
"paid_count": month_row.paid_count,
|
||||
"paid_amount": round(float(month_row.paid_amount), 2),
|
||||
},
|
||||
"recent": [
|
||||
{
|
||||
"id": o.id,
|
||||
"order_no": o.order_no,
|
||||
"user_id": o.user_id,
|
||||
"amount": o.amount,
|
||||
"credits": o.credits,
|
||||
"username": u.username,
|
||||
"amount": round(o.amount, 2),
|
||||
"credits": round(o.credits, 2),
|
||||
"payment_method": o.payment_method,
|
||||
"status": o.status,
|
||||
"status": o.status if o.status in ("pending", "paid", "cancelled", "refunded") else "cancelled",
|
||||
"trade_no": o.trade_no,
|
||||
"paid_at": o.paid_at.isoformat() if o.paid_at else None,
|
||||
"created_at": o.created_at.isoformat() if o.created_at else None,
|
||||
"paid_at": _iso(o.paid_at),
|
||||
"created_at": _iso(o.created_at),
|
||||
}
|
||||
for o in recent
|
||||
for o, u in recent_data
|
||||
],
|
||||
}
|
||||
|
||||
@@ -544,13 +621,13 @@ async def get_admin_payment_orders(
|
||||
"id": o.id,
|
||||
"order_no": o.order_no,
|
||||
"user_id": o.user_id,
|
||||
"amount": o.amount,
|
||||
"credits": o.credits,
|
||||
"amount": round(o.amount, 2),
|
||||
"credits": round(o.credits, 2),
|
||||
"payment_method": o.payment_method,
|
||||
"status": o.status,
|
||||
"status": o.status if o.status in ("pending", "paid", "cancelled", "refunded") else "cancelled",
|
||||
"trade_no": o.trade_no,
|
||||
"paid_at": o.paid_at.isoformat() if o.paid_at else None,
|
||||
"created_at": o.created_at.isoformat() if o.created_at else None,
|
||||
"paid_at": _iso(o.paid_at),
|
||||
"created_at": _iso(o.created_at),
|
||||
}
|
||||
for o in orders
|
||||
],
|
||||
@@ -585,6 +662,19 @@ async def update_payment_config(
|
||||
}
|
||||
|
||||
|
||||
@router.post("/payment-orders/{order_no}/refund")
|
||||
async def refund_payment_order(
|
||||
order_no: str,
|
||||
admin: User = Depends(get_admin_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Refund a paid payment order."""
|
||||
result = await process_refund(db, order_no)
|
||||
if not result.get("success"):
|
||||
raise HTTPException(status_code=400, detail=result.get("message", "退款失败"))
|
||||
return result
|
||||
|
||||
|
||||
# ── Industry Config ──────────────────────────────────────
|
||||
|
||||
def _serialize_industry(ind: IndustryConfig) -> dict:
|
||||
@@ -1371,72 +1461,4 @@ async def admin_generate_video(
|
||||
|
||||
# ── Payment Stats ────────────────────────────────────────
|
||||
|
||||
@router.get("/payment-stats")
|
||||
async def get_payment_stats(
|
||||
admin: User = Depends(get_admin_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Payment statistics for admin dashboard."""
|
||||
from app.models.payment_order import PaymentOrder
|
||||
from datetime import datetime
|
||||
|
||||
# Count and revenue by status
|
||||
rows = (await db.execute(
|
||||
select(
|
||||
PaymentOrder.status,
|
||||
PaymentOrder.payment_method,
|
||||
func.count(PaymentOrder.id).label("count"),
|
||||
func.coalesce(func.sum(PaymentOrder.amount), 0).label("total_amount"),
|
||||
).group_by(PaymentOrder.status, PaymentOrder.payment_method)
|
||||
)).all()
|
||||
|
||||
by_status: dict[str, dict] = {}
|
||||
for r in rows:
|
||||
s = r.status
|
||||
if s not in by_status:
|
||||
by_status[s] = {"count": 0, "amount": 0.0}
|
||||
by_status[s]["count"] += r.count
|
||||
by_status[s]["amount"] += float(r.total_amount)
|
||||
|
||||
# Recent orders (last 50)
|
||||
recent = (await db.execute(
|
||||
select(PaymentOrder)
|
||||
.order_by(PaymentOrder.created_at.desc())
|
||||
.limit(50)
|
||||
)).scalars().all()
|
||||
|
||||
# Today stats
|
||||
today_start = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
today_paid = (await db.execute(
|
||||
select(
|
||||
func.count(PaymentOrder.id),
|
||||
func.coalesce(func.sum(PaymentOrder.amount), 0),
|
||||
).where(
|
||||
PaymentOrder.status == "paid",
|
||||
PaymentOrder.paid_at >= today_start,
|
||||
)
|
||||
)).first()
|
||||
today_count, today_amount = (today_paid or (0, 0))
|
||||
|
||||
return {
|
||||
"by_status": by_status,
|
||||
"today": {
|
||||
"paid_count": int(today_count or 0),
|
||||
"paid_amount": float(today_amount or 0),
|
||||
},
|
||||
"recent": [
|
||||
{
|
||||
"id": o.id,
|
||||
"order_no": o.order_no,
|
||||
"user_id": o.user_id,
|
||||
"amount": o.amount,
|
||||
"credits": o.credits,
|
||||
"payment_method": o.payment_method,
|
||||
"status": o.status,
|
||||
"trade_no": o.trade_no,
|
||||
"created_at": _iso(o.created_at),
|
||||
"paid_at": _iso(o.paid_at),
|
||||
}
|
||||
for o in recent
|
||||
],
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
logger = logging.getLogger("videogen")
|
||||
logger = logging.getLogger("payment")
|
||||
|
||||
from app.dependencies import get_db, get_current_user
|
||||
from app.models.user import User
|
||||
@@ -16,13 +16,20 @@ from app.services.payment import (
|
||||
verify_wechat_callback,
|
||||
verify_alipay_callback,
|
||||
process_payment_success_by_order_no,
|
||||
process_refund,
|
||||
_get_payment_configs,
|
||||
_close_alipay_order,
|
||||
_get_order_expire_seconds,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/payments", tags=["payments"])
|
||||
|
||||
|
||||
@router.get("/methods")
|
||||
async def get_payment_methods(db: AsyncSession = Depends(get_db)):
|
||||
async def get_payment_methods(
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Return which payment methods are enabled (from admin config)."""
|
||||
from app.services.payment import _get_payment_configs
|
||||
configs = await _get_payment_configs(db)
|
||||
@@ -89,9 +96,10 @@ async def wechat_callback(request: Request, db: AsyncSession = Depends(get_db)):
|
||||
async def alipay_callback(request: Request, db: AsyncSession = Depends(get_db)):
|
||||
form_data = await request.form()
|
||||
data = dict(form_data)
|
||||
|
||||
logger.info(
|
||||
f"ALIPAY_CALLBACK order_no={data.get('out_trade_no')} "
|
||||
f"trade_no={data.get('trade_no', '')} status={data.get('trade_status', '')}"
|
||||
f"data={data}"
|
||||
)
|
||||
|
||||
# Verify signature first
|
||||
@@ -106,8 +114,11 @@ async def alipay_callback(request: Request, db: AsyncSession = Depends(get_db)):
|
||||
|
||||
order_no = data.get("out_trade_no")
|
||||
trade_no = data.get("trade_no", "")
|
||||
total_amount_str = data.get("total_amount", "")
|
||||
total_amount = float(total_amount_str) if total_amount_str else None
|
||||
|
||||
if order_no:
|
||||
await process_payment_success_by_order_no(db, order_no, trade_no)
|
||||
await process_payment_success_by_order_no(db, order_no, trade_no, total_amount)
|
||||
|
||||
return "success"
|
||||
|
||||
@@ -130,6 +141,29 @@ async def list_orders(
|
||||
return orders
|
||||
|
||||
|
||||
@router.get("/orders/{order_no}", response_model=PaymentOrderOut)
|
||||
async def get_order(
|
||||
order_no: str,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
from app.services.payment import _check_and_expire_order
|
||||
result = await db.execute(
|
||||
select(PaymentOrder)
|
||||
.where(
|
||||
PaymentOrder.order_no == order_no,
|
||||
PaymentOrder.user_id == current_user.id,
|
||||
)
|
||||
.limit(1)
|
||||
)
|
||||
order = result.scalar_one_or_none()
|
||||
if not order:
|
||||
raise HTTPException(status_code=404, detail="订单不存在")
|
||||
# Auto-expire if needed
|
||||
await _check_and_expire_order(db, order)
|
||||
return order
|
||||
|
||||
|
||||
@router.post("/orders/{order_no}/cancel")
|
||||
async def cancel_order(
|
||||
order_no: str,
|
||||
@@ -148,6 +182,15 @@ async def cancel_order(
|
||||
raise HTTPException(status_code=404, detail="订单不存在")
|
||||
if order.status != "pending":
|
||||
raise HTTPException(status_code=400, detail=f"订单状态为{order.status},无法取消")
|
||||
|
||||
# If it's an Alipay order, call close API first
|
||||
if order.payment_method == "alipay":
|
||||
db_configs = await _get_payment_configs(db)
|
||||
try:
|
||||
await _close_alipay_order(db, order, db_configs)
|
||||
except Exception as e:
|
||||
logger.exception(f"Failed to close Alipay order {order_no}: {e}")
|
||||
|
||||
order.status = "cancelled"
|
||||
await db.flush()
|
||||
logger.info(
|
||||
|
||||
@@ -41,7 +41,7 @@ async def create_app(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
try:
|
||||
app = await create_user_oauth_app(db, req.app_id, req.secret, req.open_type, admin.id)
|
||||
app = await create_user_oauth_app(db, req.app_id, req.secret, req.open_type, admin.id, req.count, req.auth_url, req.company)
|
||||
return UserOAuthAppOut.model_validate(app)
|
||||
except ValueError as e:
|
||||
raise HTTPException(
|
||||
@@ -72,7 +72,7 @@ async def update_app(
|
||||
admin: User = Depends(get_admin_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
app = await update_user_oauth_app(db, id, req.secret, req.open_type, req.status, admin.id)
|
||||
app = await update_user_oauth_app(db, id, req.secret, req.open_type, req.status, req.count, req.auth_url, req.company, admin.id)
|
||||
if not app:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
|
||||
Reference in New Issue
Block a user