feat: 管理后台完整可用 + 注册登录记日志 + 提取信息结构化展示 + 微信配置就绪
- 管理后台用户/统计/日志/配置四页签全部对接真实后端API - auth注册/登录/游客/微信登录事件写入usage_logs表 - 提取信息结果从原始JSON改为卡片式字段列表(中文标签) - 管理后台搜索按钮增加加载态和结果数提示 - 配置WECHAT_APP_ID/WECHAT_APP_SECRET - 客户/产品/报价单CRUD页面完整(导出导入批量操作)
This commit is contained in:
@@ -1,10 +1,21 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Response, UploadFile, File
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from typing import Optional
|
||||
from typing import Optional, List
|
||||
from app.database import get_db
|
||||
from app.services.product import ProductService
|
||||
from app.services import export
|
||||
from app.api.v1.deps import get_current_user_id
|
||||
from pydantic import BaseModel
|
||||
import io
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
import openpyxl
|
||||
HAS_OPENPYXL = True
|
||||
except ImportError:
|
||||
HAS_OPENPYXL = False
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -50,6 +61,101 @@ async def list_products(
|
||||
return await service.list_products(user_id, category, page, size)
|
||||
|
||||
|
||||
@router.get("/export/csv")
|
||||
async def export_products(
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
service = ProductService(db)
|
||||
result = await service.list_products(user_id, None, 1, 9999)
|
||||
items = result.get("items", [])
|
||||
csv_bytes = export.export_products_csv(items)
|
||||
return Response(
|
||||
content=csv_bytes,
|
||||
media_type="text/csv",
|
||||
headers={"Content-Disposition": "attachment; filename=products.csv"},
|
||||
)
|
||||
|
||||
|
||||
@router.get("/export/xlsx")
|
||||
async def export_products_xlsx(
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
service = ProductService(db)
|
||||
result = await service.list_products(user_id, None, 1, 9999)
|
||||
items = result.get("items", [])
|
||||
xlsx_bytes = export.export_products_xlsx(items)
|
||||
return Response(
|
||||
content=xlsx_bytes,
|
||||
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
headers={"Content-Disposition": "attachment; filename=products.xlsx"},
|
||||
)
|
||||
|
||||
|
||||
@router.post("/import")
|
||||
async def import_products(
|
||||
file: UploadFile = File(...),
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
service = ProductService(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
|
||||
records.append({
|
||||
"name": str(row[0] or ""),
|
||||
"name_en": str(row[1] or ""),
|
||||
"category": str(row[2] or ""),
|
||||
"description": str(row[3] or ""),
|
||||
"price": str(row[4] or ""),
|
||||
"price_unit": str(row[5] or "USD") if len(row) > 5 else "USD",
|
||||
"moq": str(row[6] or "") if len(row) > 6 else "",
|
||||
"keywords": [k.strip() for k in str(row[7] or "").split(",") if k.strip()] if len(row) > 7 else [],
|
||||
})
|
||||
elif filename.endswith(".csv"):
|
||||
import csv
|
||||
reader = csv.DictReader(io.StringIO(content.decode("utf-8-sig")))
|
||||
records = []
|
||||
for row in reader:
|
||||
name = row.get("名称", row.get("name", "")).strip()
|
||||
if not name:
|
||||
continue
|
||||
records.append({
|
||||
"name": name,
|
||||
"name_en": row.get("英文名", row.get("name_en", "")),
|
||||
"category": row.get("分类", row.get("category", "")),
|
||||
"description": row.get("描述", row.get("description", "")),
|
||||
"price": row.get("价格", row.get("price", "")),
|
||||
"price_unit": row.get("货币", row.get("price_unit", "USD")),
|
||||
"moq": row.get("MOQ", row.get("moq", "")),
|
||||
"keywords": [k.strip() for k in row.get("关键词", row.get("keywords", "")).split(",") if k.strip()],
|
||||
})
|
||||
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_product(user_id, rec)
|
||||
imported += 1
|
||||
except Exception as e:
|
||||
errors.append(f"{rec.get('name', '')}: {str(e)}")
|
||||
|
||||
return {"imported": imported, "errors": errors}
|
||||
|
||||
|
||||
@router.get("/{product_id}")
|
||||
async def get_product(
|
||||
product_id: str,
|
||||
|
||||
Reference in New Issue
Block a user