diff --git a/backend/app/api/customers.py b/backend/app/api/customers.py index 2d72dbc..a7a1db8 100644 --- a/backend/app/api/customers.py +++ b/backend/app/api/customers.py @@ -5,7 +5,12 @@ 处理客户信息的增删改查和批量导入接口,URL 前缀为 /api/customers。 包括:客户列表查询、创建客户、查看客户详情、更新客户、批量导入客户。 """ -from fastapi import APIRouter, Depends, Query +import os +import tempfile +from io import BytesIO + +from fastapi import APIRouter, Depends, File, Query, UploadFile +from fastapi.responses import StreamingResponse from sqlalchemy.orm import Session from backend.app.api.deps import require_roles @@ -13,7 +18,7 @@ from backend.app.core.error_codes import ErrorCode from backend.app.core.exceptions import AppException from backend.app.db import get_db_session from backend.app.schemas.common import success_payload -from backend.app.schemas.customers import CreateCustomerRequest, ImportCustomerRequest, UpdateCustomerRequest +from backend.app.schemas.customers import CreateCustomerRequest, UpdateCustomerRequest from backend.app.services.customer_service import customer_service router = APIRouter(prefix="/api/customers", tags=["customers"]) @@ -119,17 +124,75 @@ def update_customer( return success_payload(result) +@router.get("/import-template") +def download_import_template( + _user: dict = Depends(require_roles("manager", "admin")), # 角色鉴权:仅经理/管理员 +) -> StreamingResponse: + """下载客户导入模板 + + 用途:生成包含表头和示例行的 Excel 模板供用户下载填写。 + 返回值:xlsx 文件流,浏览器自动触发下载。 + 权限要求:经理(manager)、管理员(admin)。 + """ + from openpyxl import Workbook + + wb = Workbook() + ws = wb.active + ws.title = "客户导入模板" + headers = ["客户姓名", "手机号", "客户地址", "客户类型", "结算方式", "账期天数", "业务员ID", "信用额度", "备注"] + ws.append(headers) + ws.append(["张三", "13800000000", "广州市天河区xxx", "渠道客户", "月结", 30, "", 0, "示例数据,请删除后填写"]) + for cell in ws[1]: + cell.font = cell.font.copy(bold=True) + ws.column_dimensions["A"].width = 14 + ws.column_dimensions["B"].width = 16 + ws.column_dimensions["C"].width = 28 + ws.column_dimensions["D"].width = 14 + ws.column_dimensions["E"].width = 14 + ws.column_dimensions["F"].width = 12 + ws.column_dimensions["G"].width = 12 + ws.column_dimensions["H"].width = 12 + ws.column_dimensions["I"].width = 30 + + buf = BytesIO() + wb.save(buf) + buf.seek(0) + return StreamingResponse( + buf, + media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + headers={"Content-Disposition": "attachment; filename=customer_import_template.xlsx"}, + ) + + @router.post("/import") def import_customers( - payload: ImportCustomerRequest, # 批量导入请求体 - session: Session = Depends(get_db_session), # 注入数据库会话 - current_user: dict = Depends(require_roles("manager", "admin")), # 角色鉴权:仅经理/管理员 + file: UploadFile = File(...), # 上传的 Excel/CSV 文件 + import_mode: str = Query(default="skip_duplicate"), # 导入模式:skip_duplicate/cover_duplicate + session: Session = Depends(get_db_session), # 注入数据库会话 + _user: dict = Depends(require_roles("manager", "admin")), # 角色鉴权:仅经理/管理员 ) -> dict: - """批量导入客户 + """批量导入客户(文件上传) - 用途:通过文件批量导入客户数据(如 Excel)。 - 请求参数:ImportCustomerRequest(包含导入文件内容或解析后的数据)。 + 用途:通过上传 Excel/CSV 文件批量导入客户数据。 + 请求参数:file - 上传的文件,import_mode - 导入模式(Query 参数)。 返回值:导入结果(成功数量、失败详情等)。 权限要求:经理(manager)、管理员(admin)。 """ - return success_payload(customer_service.import_customers_from_file(session, payload.model_dump())) + if import_mode not in {"skip_duplicate", "cover_duplicate"}: + raise AppException(code=ErrorCode.PARAM_ERROR, message="导入模式不正确", status_code=400) + + suffix = "" + if file.filename: + suffix = os.path.splitext(file.filename)[1].lower() + if suffix not in {".csv", ".xlsx", ".xls"}: + raise AppException(code=ErrorCode.PARAM_ERROR, message="仅支持 csv、xlsx、xls 文件", status_code=400) + + with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tmp: + tmp.write(file.file.read()) + tmp_path = tmp.name + + return success_payload( + customer_service.import_customers_from_file( + session, {"file_url": tmp_path, "import_mode": import_mode} + ) + ) diff --git a/frontend/web-admin/src/views/CustomerCenterPage.vue b/frontend/web-admin/src/views/CustomerCenterPage.vue index f61cd9b..adddf4e 100644 --- a/frontend/web-admin/src/views/CustomerCenterPage.vue +++ b/frontend/web-admin/src/views/CustomerCenterPage.vue @@ -135,11 +135,14 @@