Files
trade-assistant/backend/app/api/v1/product.py
T
TradeMate Dev 2a107a42f3 feat: credit-based billing system
- New DB models: credit_packages, subscription_plans, user_credits, credit_consumptions, credit_purchases
- CreditService: balance, deduct, add_credits, grant_free_trial, history
- User API: /api/v1/credits/* (balance/history/packages/purchase/subscribe)
- Admin API: /api/v1/admin/credit-* (CRUD packages/plans, user credits, consumptions)
- PaymentService.create_credit_order + handle_callback for credit purchases
- Credit deduction on: discovery, translate, marketing, ai_chat, followup
- Free trial 30 credits on registration
- Documentation: docs/CREDIT_SYSTEM.md
2026-06-12 10:39:45 +08:00

230 lines
7.4 KiB
Python

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.services.usage import UsageService
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),
):
from app.services.product import ProductService
from app.config import settings
MAX_UPLOAD_SIZE = settings.MAX_UPLOAD_SIZE
filename = file.filename or "unknown"
file_size = 0
content = b""
while True:
chunk = await file.read(8192)
if not chunk:
break
file_size += len(chunk)
if file_size > MAX_UPLOAD_SIZE:
raise HTTPException(status_code=413, detail=f"File too large. Max {MAX_UPLOAD_SIZE // (1024*1024)}MB")
content += chunk
service = ProductService(db)
filename_lower = 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),
):
usage = UsageService(db)
ok, msg = await usage.check_quota(user_id, "create_product")
if not ok:
raise HTTPException(status_code=429, detail=msg)
service = ProductService(db)
product = await service.create_product(user_id, data.dict())
await usage.record_usage(user_id, "create_product")
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"}