from fastapi import APIRouter, Depends, HTTPException, Query, Response, UploadFile, File from sqlalchemy.ext.asyncio import AsyncSession 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() class ProductCreate(BaseModel): name: str name_en: Optional[str] = None description: Optional[str] = None description_en: Optional[str] = None category: Optional[str] = None price: Optional[str] = None price_unit: Optional[str] = "USD" moq: Optional[str] = None keywords: Optional[list] = [] specifications: Optional[dict] = {} images: Optional[list] = [] class ProductUpdate(BaseModel): name: Optional[str] = None name_en: Optional[str] = None description: Optional[str] = None description_en: Optional[str] = None category: Optional[str] = None price: Optional[str] = None price_unit: Optional[str] = None moq: Optional[str] = None keywords: Optional[list] = None specifications: Optional[dict] = None images: Optional[list] = None is_active: Optional[bool] = None @router.get("") async def list_products( category: Optional[str] = None, page: int = Query(1, ge=1), size: int = Query(20, ge=1, le=100), user_id: str = Depends(get_current_user_id), db: AsyncSession = Depends(get_db), ): service = ProductService(db) 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, user_id: str = Depends(get_current_user_id), db: AsyncSession = Depends(get_db), ): service = ProductService(db) product = await service.get_product(user_id, product_id) if not product: raise HTTPException(status_code=404, detail="Product not found") return product @router.post("") async def create_product( data: ProductCreate, user_id: str = Depends(get_current_user_id), db: AsyncSession = Depends(get_db), ): service = ProductService(db) product = await service.create_product(user_id, data.dict()) return product @router.patch("/{product_id}") async def update_product( product_id: str, data: ProductUpdate, user_id: str = Depends(get_current_user_id), db: AsyncSession = Depends(get_db), ): service = ProductService(db) product = await service.update_product(user_id, product_id, data.dict(exclude_unset=True)) if not product: raise HTTPException(status_code=404, detail="Product not found") return product @router.delete("/{product_id}") async def delete_product( product_id: str, user_id: str = Depends(get_current_user_id), db: AsyncSession = Depends(get_db), ): service = ProductService(db) deleted = await service.delete_product(user_id, product_id) if not deleted: raise HTTPException(status_code=404, detail="Product not found") return {"message": "Product deleted"}