Download apps/oa_filing/api/client_import_api.py from jun2024/fachuan: direct link, hf CLI and curl.
- Browser
- Download file 9.29 kB
-
https://huggingface.co/spaces/jun2024/fachuan/resolve/main/apps/oa_filing/api/client_import_api.py
- Command line
-
hf download hf://spaces/jun2024/fachuan/apps/oa_filing/api/client_import_api.py
-
curl -L -o client_import_api.py https://huggingface.co/spaces/jun2024/fachuan/resolve/main/apps/oa_filing/api/client_import_api.py
9.29 kB
| """客户导入 API。""" | |
| from __future__ import annotations | |
| import logging | |
| import os | |
| from typing import Any | |
| from django.http import HttpRequest | |
| from ninja import Router | |
| from apps.oa_filing.schemas.client_import_schemas import ClientImportSessionOut | |
| logger = logging.getLogger("apps.oa_filing.api.client_import") | |
| router = Router() | |
| # Django-Q 默认 timeout=600 秒;OA 客户导入通常需要更长时间。 | |
| # 保持小于默认 retry(1200) 以降低重复执行风险。 | |
| CLIENT_IMPORT_TASK_TIMEOUT_SECONDS = int(os.environ.get("OA_CLIENT_IMPORT_TASK_TIMEOUT_SECONDS", "1100") or "1100") | |
| def trigger_client_import(request: HttpRequest) -> Any: | |
| """触发从OA导入客户。""" | |
| import json | |
| from django.db.models import Q | |
| from apps.oa_filing.models import ClientImportSession | |
| from apps.organization.models import AccountCredential | |
| if not request.user.is_authenticated: | |
| return {"error": "未登录"} | |
| lawyer_id = getattr(request.user, "id", None) | |
| if lawyer_id is None: | |
| return {"error": "无效用户"} | |
| headless = True | |
| limit: int | None = None | |
| raw_body = (request.body or b"").strip() | |
| if raw_body: | |
| try: | |
| payload = json.loads(raw_body) | |
| if isinstance(payload, dict): | |
| if "headless" in payload: | |
| headless = bool(payload.get("headless")) | |
| if "limit" in payload: | |
| raw_limit = payload.get("limit") | |
| if raw_limit not in (None, "", 0, "0"): | |
| try: | |
| parsed_limit = int(raw_limit) | |
| except (TypeError, ValueError): | |
| return {"error": "导入数量必须是大于 0 的整数"} | |
| if parsed_limit <= 0: | |
| return {"error": "导入数量必须是大于 0 的整数"} | |
| limit = parsed_limit | |
| except Exception: | |
| # 非 JSON 请求体时使用默认值 | |
| headless = True | |
| limit = None | |
| # 查找用户的 jtn.com 凭证 | |
| credential = AccountCredential.objects.filter( | |
| Q(account__icontains="jtn.com") | Q(url__icontains="jtn.com"), | |
| lawyer_id=lawyer_id, | |
| ).first() | |
| if not credential: | |
| return {"error": "未找到金诚同达OA账号凭证"} | |
| # 创建导入会话 | |
| from apps.organization.models import Lawyer | |
| lawyer = Lawyer.objects.get(pk=lawyer_id) | |
| session = ClientImportSession.objects.create( | |
| lawyer=lawyer, | |
| credential=credential, | |
| status="pending", | |
| ) | |
| # 启动后台任务 | |
| from django_q.tasks import async_task | |
| async_task( | |
| "apps.oa_filing.tasks.run_client_import_task", | |
| session.id, | |
| headless=headless, | |
| limit=limit, | |
| timeout=CLIENT_IMPORT_TASK_TIMEOUT_SECONDS, | |
| task_name=f"oa_client_import_{session.id}", | |
| ) | |
| logger.info("创建客户导入会话: session_id=%d headless=%s limit=%s", session.id, headless, limit) | |
| return session | |
| def get_client_import_session(request: HttpRequest, session_id: int) -> Any: | |
| """查询客户导入会话状态。""" | |
| from apps.oa_filing.models import ClientImportSession | |
| return ClientImportSession.objects.get(pk=session_id) | |
| def batch_create_clients(request: HttpRequest, session_id: int) -> dict[str, Any]: | |
| """批量创建客户(通过API调用避免异步上下文问题)。 | |
| 接收一个客户数据列表,在新的HTTP请求中处理,不受Django-Q异步上下文影响。 | |
| 对于企业客户且id_number为空的,自动调用企业数据API补全信息。 | |
| """ | |
| from django.http import JsonResponse | |
| from apps.client.models import Client | |
| from apps.oa_filing.models import ClientImportSession | |
| # 获取会话 | |
| try: | |
| session = ClientImportSession.objects.get(pk=session_id) | |
| except ClientImportSession.DoesNotExist: | |
| return JsonResponse({"error": "会话不存在"}, status=404) | |
| # 解析请求体 | |
| import json | |
| try: | |
| body = json.loads(request.body) | |
| customers = body.get("customers", []) | |
| except Exception: | |
| return JsonResponse({"error": "无效的请求数据"}, status=400) | |
| success_count = 0 | |
| skip_count = 0 | |
| error_count = 0 | |
| enriched_count = 0 # 通过企业数据API补全的数量 | |
| errors = [] | |
| for i, customer in enumerate(customers): | |
| try: | |
| name = customer.get("name", "").strip() | |
| client_type = customer.get("client_type", "natural") | |
| phone = customer.get("phone") or "" | |
| address = customer.get("address") or "" | |
| id_number_raw = customer.get("id_number") | |
| # 空字符串转为None,避免唯一约束冲突 | |
| id_number = id_number_raw if id_number_raw and id_number_raw.strip() else None | |
| legal_representative = customer.get("legal_representative") or "" | |
| if not name: | |
| continue | |
| logger.info("[%d/%d] 处理: %s (type=%s)", i + 1, len(customers), name, client_type) | |
| # 检查是否已存在(按名称去重) | |
| if Client.objects.filter(name=name).exists(): | |
| skip_count += 1 | |
| continue | |
| # 对于自然人,还要检查id_number是否已存在(避免身份证号冲突) | |
| if client_type == "natural" and id_number: | |
| if Client.objects.filter(id_number=id_number).exists(): | |
| skip_count += 1 | |
| continue | |
| # 对于企业客户且id_number为空的,调用企业数据API补全(暂时禁用,加快导入速度) | |
| # if client_type == "legal" and not id_number: | |
| # prefill = _enrich_enterprise_data(name) | |
| # if prefill: | |
| # phone = prefill.get("phone") or phone | |
| # address = prefill.get("address") or address | |
| # id_number = prefill.get("id_number") or id_number | |
| # legal_representative = prefill.get("legal_representative") or legal_representative | |
| # enriched_count += 1 | |
| # logger.info(" -> 企业数据补全成功: phone=%s, address=%s", phone, address) | |
| # 创建客户 | |
| Client.objects.create( | |
| name=name, | |
| client_type=client_type, | |
| phone=phone, | |
| address=address, | |
| id_number=id_number, | |
| legal_representative=legal_representative, | |
| is_our_client=True, | |
| ) | |
| success_count += 1 | |
| except Exception as exc: | |
| error_count += 1 | |
| errors.append({"name": customer.get("name", ""), "error": str(exc)}) | |
| logger.warning(" -> 创建客户失败: %s", exc) | |
| # 更新会话状态 | |
| session.success_count = success_count | |
| session.skip_count = skip_count | |
| session.save() | |
| logger.info( | |
| "导入完成: 成功=%d, 跳过=%d, 错误=%d, 企业数据补全=%d", success_count, skip_count, error_count, enriched_count | |
| ) | |
| return JsonResponse( | |
| { | |
| "success_count": success_count, | |
| "skip_count": skip_count, | |
| "error_count": error_count, | |
| "enriched_count": enriched_count, | |
| "errors": errors[:10], # 最多返回10个错误 | |
| } | |
| ) | |
| def _enrich_enterprise_data(company_name: str) -> dict[str, Any] | None: | |
| """调用企业数据API补全企业信息。""" | |
| from apps.client.services.client_enterprise_prefill_service import ClientEnterprisePrefillService | |
| try: | |
| service = ClientEnterprisePrefillService() | |
| # 1. 搜索企业获取company_id | |
| search_result = service.search_companies(keyword=company_name, limit=5) | |
| items = search_result.get("items", []) | |
| if not items: | |
| logger.info(" -> 未找到企业: %s", company_name) | |
| return None | |
| # 2. 找到最匹配的企业 | |
| matched_item = None | |
| for item in items: | |
| item_name = item.get("company_name", "") | |
| if item_name == company_name: | |
| matched_item = item | |
| break | |
| if not matched_item: | |
| # 如果没有精确匹配,取第一个 | |
| matched_item = items[0] | |
| logger.info(" -> 未精确匹配,使用第一个候选: %s", matched_item.get("company_name")) | |
| company_id = matched_item.get("company_id") | |
| if not company_id: | |
| return None | |
| # 3. 获取企业详细信息 | |
| prefill_result = service.build_prefill(company_id=company_id) | |
| prefill = prefill_result.get("prefill", {}) | |
| logger.info( | |
| " -> 获取到企业信息: %s, id_number=%s, phone=%s", | |
| prefill.get("name"), | |
| prefill.get("id_number"), | |
| prefill.get("phone"), | |
| ) | |
| return prefill | |
| except Exception as exc: | |
| logger.warning(" -> 企业数据查询失败: %s", exc) | |
| return None | |