Files
crm_project/server/app/api/import_export.py
T
hankin 423baff73b v0.1.0: CRM/ERP 系统内测版本 - 安全加固完成
- Docker bridge 网络隔离(8000 端口封死)
- Gunicorn 4 Worker 多进程
- Alembic 数据库迁移基线
- 日志轮转 20m×3
- JWT 密钥 + DB 密码 + CORS 收紧
- 3-2-1 备份链路(NAS + R740-B 冷备)
- 连接池 pool_pre_ping + pool_recycle=3600
2026-03-16 07:31:37 +00:00

226 lines
7.4 KiB
Python

"""
批量导入/导出路由 —— 产品导入 / 客户导入 / 客户导出 / 模板下载
"""
from __future__ import annotations
import io
import os
import uuid
from datetime import datetime
from fastapi import APIRouter, Depends, File, UploadFile
from fastapi.responses import FileResponse, StreamingResponse
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
from app.api.deps import get_current_user
from app.db.database import get_db
from app.schemas.auth import CurrentUserPayload
from app.schemas.response import ok
from app.core.exceptions import BizException, ForbiddenException
router = APIRouter(tags=["批量导入导出"])
# ── 模板下载 ──────────────────────────────────────────
TEMPLATES_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "templates")
@router.get("/templates/{name}", summary="下载 Excel 导入模板")
async def download_template(
name: str,
):
allowed = {
"product_import_template.xlsx",
"customer_import_template.xlsx",
}
if name not in allowed:
raise BizException(message=f"模板 {name} 不存在")
file_path = os.path.join(TEMPLATES_DIR, name)
if not os.path.exists(file_path):
raise BizException(message=f"模板文件 {name} 未找到")
return FileResponse(
path=file_path,
filename=name,
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
)
# ── 产品批量导入 ──────────────────────────────────────
@router.post("/products/import", summary="Excel 批量导入产品 SKU")
async def import_products(
file: UploadFile = File(...),
db: AsyncSession = Depends(get_db),
current_user: CurrentUserPayload = Depends(get_current_user),
):
from openpyxl import load_workbook
from app.models.erp import ErpProductSku
content = await file.read()
wb = load_workbook(io.BytesIO(content))
ws = wb.active
if ws is None:
raise BizException(message="Excel 文件无可用工作表")
rows = list(ws.iter_rows(min_row=2, values_only=True)) # 跳过表头
if not rows:
raise BizException(message="Excel 中无数据行")
created = 0
skipped = 0
errors = []
for i, row in enumerate(rows, start=2):
try:
sku_code = str(row[0] or "").strip()
name = str(row[1] or "").strip()
spec = str(row[2] or "").strip() or None
standard_price = float(row[3] or 0)
unit = str(row[4] or "").strip()
warning_threshold = float(row[5] or 0)
if not sku_code or not name:
skipped += 1
continue
# 检查 sku_code 是否已存在
exists = (await db.execute(
select(func.count()).select_from(ErpProductSku).where(
ErpProductSku.sku_code == sku_code,
ErpProductSku.is_deleted.is_(False),
)
)).scalar()
if exists:
skipped += 1
continue
sku = ErpProductSku(
sku_code=sku_code,
name=name,
spec=spec,
standard_price=standard_price,
unit=unit,
warning_threshold=warning_threshold,
)
db.add(sku)
created += 1
except Exception as e:
errors.append(f"{i}行: {e!s}")
await db.commit()
return ok(
data={"created": created, "skipped": skipped, "errors": errors},
message=f"导入完成:新增 {created} 条,跳过 {skipped}",
)
# ── 客户批量导入 ──────────────────────────────────────
@router.post("/crm/import", summary="Excel 批量导入客户")
async def import_customers(
file: UploadFile = File(...),
db: AsyncSession = Depends(get_db),
current_user: CurrentUserPayload = Depends(get_current_user),
):
from openpyxl import load_workbook
from app.models.crm import CrmCustomer
content = await file.read()
wb = load_workbook(io.BytesIO(content))
ws = wb.active
if ws is None:
raise BizException(message="Excel 文件无可用工作表")
rows = list(ws.iter_rows(min_row=2, values_only=True))
if not rows:
raise BizException(message="Excel 中无数据行")
created = 0
skipped = 0
errors = []
for i, row in enumerate(rows, start=2):
try:
name = str(row[0] or "").strip()
level = str(row[1] or "C").strip().upper()
industry = str(row[2] or "").strip() or None
contact = str(row[3] or "").strip() or None
phone = str(row[4] or "").strip() or None
email = str(row[5] or "").strip() or None
address = str(row[6] or "").strip() or None
if not name:
skipped += 1
continue
if level not in ("A", "B", "C"):
level = "C"
customer = CrmCustomer(
name=name,
level=level,
industry=industry,
contact=contact,
phone=phone,
email=email,
address=address,
owner_id=current_user.user_id,
)
db.add(customer)
created += 1
except Exception as e:
errors.append(f"{i}行: {e!s}")
await db.commit()
return ok(
data={"created": created, "skipped": skipped, "errors": errors},
message=f"导入完成:新增 {created} 条,跳过 {skipped}",
)
# ── 客户导出(仅 admin) ─────────────────────────────
@router.get("/crm/export", summary="导出客户数据 (仅管理员)")
async def export_customers(
db: AsyncSession = Depends(get_db),
current_user: CurrentUserPayload = Depends(get_current_user),
):
# 权限校验
if current_user.data_scope != "all" and (current_user.role_name or "").lower() != "admin":
raise ForbiddenException("仅管理员可导出客户数据")
from openpyxl import Workbook
from app.models.crm import CrmCustomer
stmt = select(CrmCustomer).where(CrmCustomer.is_deleted.is_(False)).order_by(CrmCustomer.created_at.desc())
customers = (await db.execute(stmt)).scalars().all()
wb = Workbook()
ws = wb.active
ws.title = "客户列表"
ws.append(["客户名称", "等级", "行业", "联系人", "电话", "邮箱", "地址", "AI评分", "创建时间"])
for c in customers:
ws.append([
c.name,
c.level,
c.industry or "",
c.contact or "",
c.phone or "",
c.email or "",
c.address or "",
float(c.ai_score or 0),
c.created_at.strftime("%Y-%m-%d %H:%M") if c.created_at else "",
])
buffer = io.BytesIO()
wb.save(buffer)
buffer.seek(0)
filename = f"customers_export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
return StreamingResponse(
buffer,
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
headers={"Content-Disposition": f"attachment; filename={filename}"},
)