1、增加订单开发票功能和发票抬头添加功能
2、一个订单只能在一个开票里,不允许多开
This commit is contained in:
@@ -35,6 +35,7 @@ from app.api.v1.material_admin import router as material_admin_router
|
||||
from app.api.v1.private_portrait import router as private_portrait_router
|
||||
from app.api.v1.private_portrait_virtual import router as private_portrait_virtual_router
|
||||
from app.api.v1.upload_resource import router as upload_resource_router
|
||||
from app.api.v1.invoices import router as invoices_router
|
||||
|
||||
api_router = APIRouter()
|
||||
api_router.include_router(auth_router)
|
||||
@@ -72,3 +73,4 @@ api_router.include_router(material_admin_router)
|
||||
api_router.include_router(private_portrait_router)
|
||||
api_router.include_router(private_portrait_virtual_router)
|
||||
api_router.include_router(upload_resource_router)
|
||||
api_router.include_router(invoices_router)
|
||||
|
||||
@@ -60,6 +60,7 @@ from app.services.resource_signed_url_service import build_resource_signed_url
|
||||
from app.services.payment import process_refund
|
||||
from app.services.resource_capacity_service import batch_get_user_resource_capacity_usage, get_user_resource_capacity_usage
|
||||
from app.services.team_service import batch_get_team_name_map, set_frontend_user_team
|
||||
from app.schemas.invoice import InvoiceStatusUpdateRequest
|
||||
|
||||
from app.utils.id_gen import generate_id
|
||||
|
||||
@@ -1973,11 +1974,10 @@ async def get_stats(
|
||||
)
|
||||
|
||||
# ── 每日各模块积分消耗(始终返回选中日期往前7天,便于图表展示)
|
||||
# 把 timestamptz 按东八区(业务时区)偏移后再转 DATE,
|
||||
# 直接手动 +8 小时再 CAST 成日期,简单稳妥,不依赖数据库时区名配置。
|
||||
# 与代码中 CST = timezone(timedelta(hours=8)) 保持一致。
|
||||
# created_at 为 timestamptz,数据库 session 时区已是东八区(CST),
|
||||
# 读取出来的时间值即为北京时间,直接 CAST 成日期即可,无需再 +8 小时。
|
||||
from sqlalchemy import Date, cast as sa_cast
|
||||
_day_expr = sa_cast(CreditRecord.created_at + timedelta(hours=8), Date)
|
||||
_day_expr = sa_cast(CreditRecord.created_at, Date)
|
||||
# 图表固定展示 [date_end - 6天, date_end] 共7天
|
||||
_chart_end_dt = date_end
|
||||
_chart_start_dt = datetime(
|
||||
@@ -2487,6 +2487,118 @@ async def upload_login_video(
|
||||
return {"url": url}
|
||||
|
||||
|
||||
# ── Payment Stats ────────────────────────────────────────
|
||||
# ── Invoice Management ───────────────────────────────────
|
||||
|
||||
|
||||
@router.get("/invoices")
|
||||
async def admin_list_invoices(
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=500),
|
||||
status: str | None = Query(None),
|
||||
phone: str | None = Query(None, description="按用户手机号模糊搜索"),
|
||||
start_date: str | None = Query(None),
|
||||
end_date: str | None = Query(None),
|
||||
admin: User = Depends(get_admin_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""后台发票列表(分页+筛选)。"""
|
||||
from app.services.invoice import get_admin_invoices
|
||||
|
||||
items, total = await get_admin_invoices(
|
||||
db, page, page_size,
|
||||
status_filter=status,
|
||||
phone=phone,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
)
|
||||
return {"items": items, "total": total, "page": page, "page_size": page_size}
|
||||
|
||||
|
||||
@router.get("/invoices/{invoice_id}")
|
||||
async def admin_get_invoice(
|
||||
invoice_id: str,
|
||||
admin: User = Depends(get_admin_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""后台获取发票详情(含关联订单)。"""
|
||||
from app.services.invoice import get_invoice_with_orders
|
||||
|
||||
detail = await get_invoice_with_orders(db, invoice_id)
|
||||
if not detail:
|
||||
raise HTTPException(status_code=404, detail="发票不存在")
|
||||
|
||||
invoice = detail["invoice"]
|
||||
orders = detail["orders"]
|
||||
return {
|
||||
"id": invoice.id,
|
||||
"invoiceNo": invoice.invoice_no,
|
||||
"userId": invoice.user_id,
|
||||
"headerType": invoice.header_type,
|
||||
"headerName": invoice.header_name,
|
||||
"headerTaxNo": invoice.header_tax_no,
|
||||
"headerRegisterAddress": invoice.header_register_address,
|
||||
"headerRegisterPhone": invoice.header_register_phone,
|
||||
"headerBankName": invoice.header_bank_name,
|
||||
"headerBankAccount": invoice.header_bank_account,
|
||||
"email": invoice.email,
|
||||
"totalAmount": round(float(invoice.total_amount), 2),
|
||||
"totalCredits": round(float(invoice.total_credits), 2),
|
||||
"status": invoice.status,
|
||||
"failureReason": invoice.failure_reason,
|
||||
"issuedAt": invoice.issued_at.isoformat() if invoice.issued_at else None,
|
||||
"createdAt": invoice.created_at.isoformat() if invoice.created_at else None,
|
||||
"updatedAt": invoice.updated_at.isoformat() if invoice.updated_at else None,
|
||||
"orders": [
|
||||
{
|
||||
"id": o.id,
|
||||
"orderNo": o.order_no,
|
||||
"amount": round(float(o.amount), 2),
|
||||
"credits": round(float(o.credits), 2),
|
||||
}
|
||||
for o in orders
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@router.put("/invoices/{invoice_id}/status")
|
||||
async def admin_update_invoice_status(
|
||||
invoice_id: str,
|
||||
req: InvoiceStatusUpdateRequest,
|
||||
admin: User = Depends(get_admin_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""更新发票状态(success/failed)。"""
|
||||
from app.services.invoice import update_invoice_status
|
||||
|
||||
invoice, old_status = await update_invoice_status(db, invoice_id, req, admin.id)
|
||||
await db.flush()
|
||||
|
||||
await log_operation(
|
||||
db,
|
||||
admin.id,
|
||||
admin.username,
|
||||
f"发票状态变更: {invoice.invoice_no} {old_status} → {req.status}",
|
||||
"PUT",
|
||||
f"/admin/invoices/{invoice_id}/status",
|
||||
detail=json.dumps(
|
||||
{
|
||||
"invoice_id": invoice_id,
|
||||
"invoice_no": invoice.invoice_no,
|
||||
"old_status": old_status,
|
||||
"new_status": req.status,
|
||||
"failure_reason": req.failure_reason,
|
||||
},
|
||||
ensure_ascii=False,
|
||||
),
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
return {
|
||||
"id": invoice.id,
|
||||
"invoiceNo": invoice.invoice_no,
|
||||
"status": invoice.status,
|
||||
"failureReason": invoice.failure_reason,
|
||||
"issuedAt": invoice.issued_at.isoformat() if invoice.issued_at else None,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.dependencies import get_db, get_current_user
|
||||
from app.models.invoice import Invoice, InvoiceOrder
|
||||
from app.models.user import User
|
||||
from app.schemas.invoice import InvoiceCreateRequest, InvoiceOut, InvoiceOrderOut
|
||||
from app.services.invoice import (
|
||||
create_invoice,
|
||||
get_user_invoices,
|
||||
get_invoice_by_id,
|
||||
get_invoice_with_orders,
|
||||
)
|
||||
|
||||
logger = logging.getLogger("videogen")
|
||||
|
||||
router = APIRouter(prefix="/invoices", tags=["invoices"])
|
||||
|
||||
|
||||
@router.post("", response_model=InvoiceOut)
|
||||
async def create_invoice_endpoint(
|
||||
req: InvoiceCreateRequest,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""创建发票申请。"""
|
||||
invoice = await create_invoice(db, current_user.id, req)
|
||||
await db.commit()
|
||||
|
||||
# 重新查询以获取关联订单
|
||||
detail = await get_invoice_with_orders(db, invoice.id)
|
||||
return _invoice_to_out(detail["invoice"], detail["orders"])
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def list_invoices(
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=100),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""获取当前用户的发票列表(分页)。"""
|
||||
invoices, total = await get_user_invoices(db, current_user.id, page, page_size)
|
||||
|
||||
# 加载每个发票的关联订单
|
||||
items = []
|
||||
for inv in invoices:
|
||||
result = await db.execute(
|
||||
select(InvoiceOrder).where(InvoiceOrder.invoice_id == inv.id)
|
||||
)
|
||||
orders = result.scalars().all()
|
||||
items.append(_invoice_to_out(inv, list(orders)))
|
||||
|
||||
return {"items": items, "total": total, "page": page, "page_size": page_size}
|
||||
|
||||
|
||||
@router.get("/{invoice_id}")
|
||||
async def get_invoice(
|
||||
invoice_id: str,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""获取发票详情(含关联订单)。"""
|
||||
detail = await get_invoice_with_orders(db, invoice_id)
|
||||
if not detail:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="发票不存在")
|
||||
|
||||
invoice = detail["invoice"]
|
||||
if invoice.user_id != current_user.id:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="无权查看该发票")
|
||||
|
||||
return _invoice_to_out(invoice, detail["orders"])
|
||||
|
||||
|
||||
def _invoice_to_out(invoice: Invoice, orders: list[InvoiceOrder]) -> dict:
|
||||
"""将 Invoice ORM 对象转换为响应 dict。"""
|
||||
return {
|
||||
"id": invoice.id,
|
||||
"userId": invoice.user_id,
|
||||
"invoiceNo": invoice.invoice_no,
|
||||
"headerType": invoice.header_type,
|
||||
"headerName": invoice.header_name,
|
||||
"headerTaxNo": invoice.header_tax_no,
|
||||
"headerRegisterAddress": invoice.header_register_address,
|
||||
"headerRegisterPhone": invoice.header_register_phone,
|
||||
"headerBankName": invoice.header_bank_name,
|
||||
"headerBankAccount": invoice.header_bank_account,
|
||||
"email": invoice.email,
|
||||
"totalAmount": round(float(invoice.total_amount), 2),
|
||||
"totalCredits": round(float(invoice.total_credits), 2),
|
||||
"status": invoice.status,
|
||||
"failureReason": invoice.failure_reason,
|
||||
"issuedAt": invoice.issued_at.isoformat() if invoice.issued_at else None,
|
||||
"createdAt": invoice.created_at.isoformat() if invoice.created_at else None,
|
||||
"updatedAt": invoice.updated_at.isoformat() if invoice.updated_at else None,
|
||||
"orders": [
|
||||
{
|
||||
"id": o.id,
|
||||
"invoiceId": o.invoice_id,
|
||||
"orderId": o.order_id,
|
||||
"orderNo": o.order_no,
|
||||
"amount": round(float(o.amount), 2),
|
||||
"credits": round(float(o.credits), 2),
|
||||
}
|
||||
for o in orders
|
||||
],
|
||||
}
|
||||
Reference in New Issue
Block a user