feat: 管理后台完整可用 + 注册登录记日志 + 提取信息结构化展示 + 微信配置就绪
- 管理后台用户/统计/日志/配置四页签全部对接真实后端API - auth注册/登录/游客/微信登录事件写入usage_logs表 - 提取信息结果从原始JSON改为卡片式字段列表(中文标签) - 管理后台搜索按钮增加加载态和结果数提示 - 配置WECHAT_APP_ID/WECHAT_APP_SECRET - 客户/产品/报价单CRUD页面完整(导出导入批量操作)
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Response
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Response, UploadFile, File
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel
|
||||
@@ -10,6 +10,16 @@ from app.api.v1.deps import get_current_user_id
|
||||
from app.models.quotation import Quotation
|
||||
from app.models.customer import Customer
|
||||
from sqlalchemy import select, and_
|
||||
import logging
|
||||
import io
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
import openpyxl
|
||||
HAS_OPENPYXL = True
|
||||
except ImportError:
|
||||
HAS_OPENPYXL = False
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -59,6 +69,64 @@ async def list_quotations(
|
||||
return await service.list_quotations(user_id, page, size)
|
||||
|
||||
|
||||
@router.post("/import")
|
||||
async def import_quotations(
|
||||
file: UploadFile = File(...),
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
service = QuotationService(db)
|
||||
content = await file.read()
|
||||
filename = file.filename.lower()
|
||||
|
||||
if filename.endswith(".xlsx"):
|
||||
if not HAS_OPENPYXL:
|
||||
raise HTTPException(status_code=501, detail="openpyxl not installed, XLSX import unavailable")
|
||||
wb = openpyxl.load_workbook(io.BytesIO(content))
|
||||
ws = wb.active
|
||||
rows = list(ws.iter_rows(min_row=2, values_only=True))
|
||||
records = []
|
||||
for row in rows:
|
||||
if not row[0]:
|
||||
continue
|
||||
items = [{"product_name": "Imported Item", "quantity": 1, "unit_price": float(row[3]) if len(row) > 3 and row[3] else 0}]
|
||||
records.append({
|
||||
"title": str(row[0]),
|
||||
"customer_id": str(row[1]) if len(row) > 1 and row[1] else "",
|
||||
"currency": str(row[2]) if len(row) > 2 and row[2] in ("USD", "EUR", "GBP", "CNY") else "USD",
|
||||
"items": items,
|
||||
"status": "draft",
|
||||
})
|
||||
elif filename.endswith(".csv"):
|
||||
import csv
|
||||
reader = csv.DictReader(io.StringIO(content.decode("utf-8-sig")))
|
||||
records = []
|
||||
for row in reader:
|
||||
title = row.get("Title", row.get("标题", "")).strip()
|
||||
if not title:
|
||||
continue
|
||||
items = [{"product_name": "Imported Item", "quantity": 1, "unit_price": float(row.get("Total", row.get("总计", 0)))}]
|
||||
records.append({
|
||||
"title": title,
|
||||
"customer_id": row.get("Customer", row.get("客户", "")),
|
||||
"currency": row.get("Currency", row.get("货币", "USD")),
|
||||
"items": items,
|
||||
})
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail="Unsupported file format. Use .xlsx or .csv")
|
||||
|
||||
imported = 0
|
||||
errors = []
|
||||
for rec in records:
|
||||
try:
|
||||
await service.create_quotation(user_id, rec)
|
||||
imported += 1
|
||||
except Exception as e:
|
||||
errors.append(f"{rec.get('title', '')}: {str(e)}")
|
||||
|
||||
return {"imported": imported, "errors": errors}
|
||||
|
||||
|
||||
@router.get("/{quotation_id}")
|
||||
async def get_quotation(
|
||||
quotation_id: str,
|
||||
@@ -102,6 +170,22 @@ async def export_quotations(
|
||||
)
|
||||
|
||||
|
||||
@router.get("/export/xlsx")
|
||||
async def export_quotations_xlsx(
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
service = QuotationService(db)
|
||||
result = await service.list_quotations(user_id, 1, 9999)
|
||||
items = result.get("items", [])
|
||||
xlsx_bytes = export.export_quotations_xlsx(items)
|
||||
return Response(
|
||||
content=xlsx_bytes,
|
||||
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
headers={"Content-Disposition": "attachment; filename=quotations.xlsx"},
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{quotation_id}/pdf")
|
||||
async def export_quotation_pdf(
|
||||
quotation_id: str,
|
||||
|
||||
Reference in New Issue
Block a user