diff --git a/README.md b/README.md index 018b1ee..9600165 100644 --- a/README.md +++ b/README.md @@ -106,7 +106,7 @@ GET /health ```json { "status": "ok", - "version": "1.0.0" + "version": "2.0.0" } ``` @@ -370,13 +370,16 @@ curl -X POST http://localhost:5003/webhook \ ## 🔄 版本历史 -- **v1.0.0**: 初始版本 +- **v2.0.0**: + - fastapi 完全重构 + - 支持 F6 系统相关操作 + - 支持BI任务处理 + + +- **1.0.0**: 初始版本 - 实现基本的 Webhook 接口 - 支持 F6 系统相关操作 - - 支持文件上传和校验 - - 支持品牌、客户、车辆管理 - - 支持数据删除操作 - - 支持BI任务处理 + ## 📄 许可证 diff --git a/app/api/routes.py b/app/api/routes.py index f2ce006..7a8c8be 100644 --- a/app/api/routes.py +++ b/app/api/routes.py @@ -10,6 +10,7 @@ API 路由定义模块 from fastapi import APIRouter, Request, HTTPException, status, Depends from fastapi.responses import JSONResponse from fastapi.exceptions import RequestValidationError +from pydantic import ValidationError from typing import Dict, Any import json import anyio @@ -37,7 +38,7 @@ async def healthcheck(): 用于检查服务是否正常运行 """ - return HealthResponse(status="ok", version="1.0.0") + return HealthResponse(status="ok", version="2.0.0") @router.post("/webhook", response_model=WebhookResponse, tags=["业务"]) @@ -73,14 +74,14 @@ async def webhook( raw_data = await request.json() # 使用 Pydantic 进行数据验证(允许额外字段) webhook_data = WebhookRequest(**raw_data) - data = webhook_data.dict(exclude_none=True) + data = webhook_data.model_dump(exclude_none=True) except json.JSONDecodeError: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="请求体必须是有效的 JSON 格式" ) except Exception as e: - logger.warning(f"请求数据验证失败: {str(e)}") + logger.warning(f"请求数据验证失败: {str(e)}, 原始数据: {raw_data if 'raw_data' in locals() else 'N/A'}") # 如果验证失败,仍然尝试使用原始数据(向后兼容) data = raw_data if 'raw_data' in locals() else {} @@ -88,9 +89,11 @@ async def webhook( header = request.headers decoded_header = app_tools.decode_headers(header) - # 验证 Action 字段 - action = decoded_header.get('Action') + # 验证 Action 字段(HTTP头在FastAPI中会被转换为小写) + # 同时检查 'Action' 和 'action' 以兼容不同情况 + action = decoded_header.get('Action') or decoded_header.get('action') if not action: + logger.warning(f"请求头中缺少 Action 字段,请求头: {decoded_header}") raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="请求头中缺少必需的 Action 字段" @@ -98,7 +101,8 @@ async def webhook( # 处理 F6_Plugin 特殊逻辑 if action == 'F6_Plugin': - check = decoded_header.get('Check') + # 同时检查 'Check' 和 'check' 以兼容不同情况 + check = decoded_header.get('Check') or decoded_header.get('check') if check == '否': handler = f6_plugin_module.check_file elif check == '是': @@ -150,13 +154,50 @@ async def webhook( if not isinstance(result, dict): result = {"msg": str(result)} + # 处理 msg 字段:如果 msg 是字典,将其内容展开到结果中 + if "msg" in result and isinstance(result["msg"], dict): + msg_dict = result.pop("msg") + logger.warning(f"操作 {action} 返回的 msg 字段是字典类型,正在自动转换。原始数据: {json.dumps(msg_dict, ensure_ascii=False)}") + # 如果字典中有 msg 字段,使用它;否则使用 JSON 字符串 + if "msg" in msg_dict: + result["msg"] = msg_dict.pop("msg") + else: + result["msg"] = json.dumps(msg_dict, ensure_ascii=False) + # 将字典中的其他字段合并到结果中 + result.update(msg_dict) + if "msg" not in result: result["msg"] = "操作完成" + + # 确保 msg 是字符串类型 + if not isinstance(result.get("msg"), str): + logger.warning(f"操作 {action} 返回的 msg 字段类型为 {type(result.get('msg'))},正在转换为字符串") + result["msg"] = str(result.get("msg", "操作完成")) logger.info(f"操作完成: {action}, 结果: {json.dumps(result, ensure_ascii=False)}") # 返回响应(使用 Pydantic 模型验证) - return WebhookResponse(**result) + try: + return WebhookResponse(**result) + except ValidationError as validation_error: + # 捕获 Pydantic 验证错误,提供更清晰的错误信息 + error_messages = [] + for error in validation_error.errors(): + field = " -> ".join(str(loc) for loc in error.get("loc", [])) + error_type = error.get("type", "unknown") + error_msg = error.get("msg", "验证失败") + error_messages.append(f"字段 '{field}': {error_msg} (类型: {error_type})") + + error_detail = "; ".join(error_messages) + logger.error( + f"响应数据验证失败 - 操作: {action}, " + f"错误详情: {error_detail}, " + f"原始数据: {json.dumps(result, ensure_ascii=False, default=str)}" + ) + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"响应数据格式错误: {error_detail}。请检查操作 '{action}' 的返回格式是否符合 API 规范(msg 字段必须是字符串类型)。" + ) except HTTPException: # 重新抛出 HTTP 异常 diff --git a/app/module/F6_Plugin_module.py b/app/module/F6_Plugin_module.py index 1b3dfc8..43dcb50 100644 --- a/app/module/F6_Plugin_module.py +++ b/app/module/F6_Plugin_module.py @@ -278,24 +278,27 @@ class F6PluginModule: Returns: Dict[str, str]: 包含执行状态的字典 """ + print('执行 删除客户') entry_data = api_instance.entry_data_get(data=data) username = entry_data['data']['账号'] password = entry_data['data']['密码'] company_name = entry_data['data']['公司名称'] res = F6Module.login_in(username, password, company_name) + print(res.json()) if res is not None: cookies = requests.utils.dict_from_cookiejar(res.cookies) - url = "https://yunxiu.f6car.cn/member/customer/listForPermission?pageSize=50000&pageNo=1" + url = "https://yunxiu.f6car.cn/member/customer/listForPermission?pageSize=30000&pageNo=1" res = requests.get(url, cookies=cookies) - json = res.json() + total = res.json().get('data', {}).get('total', 0) - if json: + if total: + total = int(total) thread = threading.Thread(target=delete_customer_background, - args=(data, cookies, json['data']['data'],)) + args=(data, cookies, total,)) thread.start() - return {'msg': '正在执行中', 'msg_details': '8-20点3.5s一条数据,其余时间1.5s一条数据'} + return {'msg': '正在执行中', 'msg_details': f'总计{total}条数据,8-20点3.5s一条数据,其余时间1.5s一条数据'} else: return {'msg': '未执行', 'msg_details': '无客户信息'} else: diff --git a/app/tasks/delete_tasks.py b/app/tasks/delete_tasks.py index a7f0e64..505e512 100644 --- a/app/tasks/delete_tasks.py +++ b/app/tasks/delete_tasks.py @@ -59,7 +59,7 @@ def delete_history_background(data: Dict[str, Any], cookies: Dict[str, str], org print('表单已自动提交至下一步') -def delete_customer_background(data: Dict[str, Any], cookies: Dict[str, str], json_data: List[Dict[str, Any]]): +def delete_customer_background(data: Dict[str, Any], cookies: Dict[str, str], total:int ): """ 删除客户信息后台任务 @@ -78,9 +78,18 @@ def delete_customer_background(data: Dict[str, Any], cookies: Dict[str, str], js - 8-20点之间每3.5秒删除一条数据,其余时间每1.5秒删除一条数据 - 执行结果会更新到简道云表单 """ + print('开始删除客户信息') success = 0 fail = 0 + + json_data = [] + total_page = total // 100 + (total % 100 > 0) + for page in tqdm(range(1, total_page + 1)): + url = f"https://yunxiu.f6car.cn/member/customer/listForPermission?pageSize=100&pageNo={page}" + res = requests.get(url, cookies=cookies) + json_data.extend(res.json().get('data', {}).get('data',[])) + # 获取门店ID operate_org_id = get_operate_org_id(cookies) if not operate_org_id: @@ -109,21 +118,56 @@ def delete_customer_background(data: Dict[str, Any], cookies: Dict[str, str], js try: url = f"https://yunxiu.f6car.cn/member/customer/{id_customer}" # 客户信息删除url - res = requests.delete(url, cookies=cookies) # 客户信息删除 - res_data = res.json() - if res_data.get('success'): + res = requests.delete(url, cookies=cookies, timeout=10) # 客户信息删除 + + # 检查HTTP状态码 + if res.status_code != 200: + fail += 1 + error_msg = f"HTTP状态码错误: {res.status_code}" + logger.error(f"客户删除失败: ID={id_customer}, 手机号={phone}, {error_msg}") + print(f"删除失败: ID={id_customer}, 手机号={phone}, {error_msg}") + time.sleep(0.2) + continue + + # 解析响应数据 + try: + res_data = res.json() + except ValueError as json_error: + fail += 1 + error_msg = f"响应不是有效的JSON格式: {json_error}, 响应内容: {res.text[:200]}" + logger.error(f"客户删除失败: ID={id_customer}, 手机号={phone}, {error_msg}") + print(f"删除失败: ID={id_customer}, 手机号={phone}, {error_msg}") + time.sleep(0.2) + continue + + # 检查多种可能的成功标识 + # 有些API返回 success 字段,有些返回 code=200,有些返回 data 字段 + is_success = ( + res_data.get('success') is True or + res_data.get('code') == 200 or + (res_data.get('code') is None and res_data.get('data') is not None) + ) + + if is_success: success += 1 logger.info(f"客户删除成功: ID={id_customer}, 手机号={phone}") else: fail += 1 - logger.error(f"客户删除失败: ID={id_customer}, 手机号={phone}, 错误信息: {res_data.get('message')}") + error_msg = res_data.get('message') or res_data.get('msg') or '未知错误' + error_detail = f"错误信息: {error_msg}, 完整响应: {res_data}" + logger.error(f"客户删除失败: ID={id_customer}, 手机号={phone}, {error_detail}") + print(f"删除失败: ID={id_customer}, 手机号={phone}, {error_msg}") time.sleep(0.2) + except requests.exceptions.RequestException as e: + fail += 1 + error_msg = f"网络请求异常: {str(e)}" + print(f"删除失败: ID={id_customer}, 手机号={phone}, {error_msg}") + logger.error(f"删除客户时发生网络错误: ID={id_customer}, 手机号={phone}, {error_msg}") except Exception as e: fail += 1 - print("删除失败,", item, id_customer, phone, e) - logger.error(f"删除客户时发生错误: ID={id_customer}, 手机号={phone}, 错误信息: {e}") - if success + fail < len(json_data): - continue + error_msg = f"未知错误: {str(e)}" + print(f"删除失败: ID={id_customer}, 手机号={phone}, {error_msg}") + logger.error(f"删除客户时发生错误: ID={id_customer}, 手机号={phone}, {error_msg}", exc_info=True) now = datetime.now() if 8 <= now.hour <= 20: diff --git a/main.py b/main.py index 422dff8..20825a5 100644 --- a/main.py +++ b/main.py @@ -8,7 +8,7 @@ 4. 中间件配置 作者: 项目团队 -版本: 1.0.0 +版本: 2.0.0 """ from contextlib import asynccontextmanager from fastapi import FastAPI, Request, HTTPException, status @@ -90,7 +90,7 @@ async def lifespan(app: FastAPI): app = FastAPI( title="简道云FastAPI服务", description="简道云插件后端服务,提供数据同步和处理功能", - version="1.0.0", + version="2.0.0", lifespan=lifespan ) @@ -106,6 +106,26 @@ app.add_middleware( app.include_router(router) +@app.get("/", tags=["系统"]) +async def root(): + """ + 根路径端点 + + 返回服务基本信息和可用端点 + """ + return { + "service": "简道云FastAPI服务", + "version": "2.0.0", + "status": "running", + "endpoints": { + "health": "/health", + "webhook": "/webhook", + "docs": "/docs", + "redoc": "/redoc" + } + } + + @app.exception_handler(HTTPException) async def http_exception_handler(request: Request, exc: HTTPException): """ @@ -128,7 +148,7 @@ async def http_exception_handler(request: Request, exc: HTTPException): content=ErrorResponse( detail=exc.detail or "HTTP error", error_code=f"HTTP_{exc.status_code}" - ).dict(), + ).model_dump(), ) @@ -154,7 +174,7 @@ async def validation_exception_handler(request: Request, exc: RequestValidationE content=ErrorResponse( detail="请求数据验证失败", error_code="VALIDATION_ERROR" - ).dict(), + ).model_dump(), ) @@ -180,7 +200,7 @@ async def general_exception_handler(request: Request, exc: Exception): content=ErrorResponse( detail="服务器内部错误", error_code="INTERNAL_ERROR" - ).dict(), + ).model_dump(), ) diff --git a/test/客户信息删除代码更新.py b/test/客户信息删除代码更新.py new file mode 100644 index 0000000..6942932 --- /dev/null +++ b/test/客户信息删除代码更新.py @@ -0,0 +1,49 @@ +import requests + +cookies = { + 'memberSESSIONID': '43f77327-7a6b-4844-a1c6-d1acd7e0c970', + 'erpLanguage': 'zh-CN', + 'prodOrg': '11240984669917217520', + 'unp': '15865484595890778191', + 'un': '15865484595890778191', + '_up': '-NillNN-qyBEJ--t3vnSknvoOF53y_SJuMkA2n43U-daUfnArpjQjaZJ9Q3d-WrAAGgt60MgQHajHWBHMKKxj0CuWypi1JgKCFP1EPEk-HbqEvcTrYkr0wcI-fBRv-ZNHu3M-GTc1p60EX-sq-RQgeIal1HLPxpurEj9mEe9rIrrcGQ.', + 'sensorsdata2015jssdkcross': '%7B%22distinct_id%22%3A%2215865484595890778191%22%2C%22first_id%22%3A%2219a48e066e68e2-067b1e693596828-4c657b58-2073600-19a48e066e71500%22%2C%22props%22%3A%7B%22%24latest_traffic_source_type%22%3A%22%E7%9B%B4%E6%8E%A5%E6%B5%81%E9%87%8F%22%2C%22%24latest_search_keyword%22%3A%22%E6%9C%AA%E5%8F%96%E5%88%B0%E5%80%BC_%E7%9B%B4%E6%8E%A5%E6%89%93%E5%BC%80%22%2C%22%24latest_referrer%22%3A%22%22%7D%2C%22%24device_id%22%3A%2219a48e066e68e2-067b1e693596828-4c657b58-2073600-19a48e066e71500%22%7D', + 'tmall': 'false', + 'Hm_lvt_25f5e7a3a5dbb293d7dd35d5f1be8d0a': '1764311728,1764643482,1764662823,1764742943', + 'Hm_lpvt_25f5e7a3a5dbb293d7dd35d5f1be8d0a': '1764742943', + 'HMACCOUNT': '55F2182717FD6AE6', +} + +headers = { + 'accept': 'application/json, text/plain, */*', + 'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6', + 'cache-control': 'no-cache', + 'pragma': 'no-cache', + 'priority': 'u=1, i', + 'referer': 'https://yunxiu.f6car.cn/erp/view/index.html', + 'sec-ch-ua': '"Chromium";v="142", "Microsoft Edge";v="142", "Not_A Brand";v="99"', + 'sec-ch-ua-mobile': '?0', + 'sec-ch-ua-platform': '"Windows"', + 'sec-fetch-dest': 'empty', + 'sec-fetch-mode': 'cors', + 'sec-fetch-site': 'same-origin', + 'traceparent': '00-b018ff54506f4db58f1c139e9cb30525-22df753b9da6e050-01', + 'tracestate': 'rum=v2&browser&dz2uw0c5ay@e5930ea8eb782ae&273f4c9d3aeb40c586714fd3270a28e9&uid_48oaftj52d5ybkwt', + 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36 Edg/142.0.0.0', + 'x-requested-with': 'XMLHttpRequest', + # 'cookie': 'memberSESSIONID=43f77327-7a6b-4844-a1c6-d1acd7e0c970; erpLanguage=zh-CN; prodOrg=11240984669917217520; unp=15865484595890778191; un=15865484595890778191; _up=-NillNN-qyBEJ--t3vnSknvoOF53y_SJuMkA2n43U-daUfnArpjQjaZJ9Q3d-WrAAGgt60MgQHajHWBHMKKxj0CuWypi1JgKCFP1EPEk-HbqEvcTrYkr0wcI-fBRv-ZNHu3M-GTc1p60EX-sq-RQgeIal1HLPxpurEj9mEe9rIrrcGQ.; sensorsdata2015jssdkcross=%7B%22distinct_id%22%3A%2215865484595890778191%22%2C%22first_id%22%3A%2219a48e066e68e2-067b1e693596828-4c657b58-2073600-19a48e066e71500%22%2C%22props%22%3A%7B%22%24latest_traffic_source_type%22%3A%22%E7%9B%B4%E6%8E%A5%E6%B5%81%E9%87%8F%22%2C%22%24latest_search_keyword%22%3A%22%E6%9C%AA%E5%8F%96%E5%88%B0%E5%80%BC_%E7%9B%B4%E6%8E%A5%E6%89%93%E5%BC%80%22%2C%22%24latest_referrer%22%3A%22%22%7D%2C%22%24device_id%22%3A%2219a48e066e68e2-067b1e693596828-4c657b58-2073600-19a48e066e71500%22%7D; tmall=false; Hm_lvt_25f5e7a3a5dbb293d7dd35d5f1be8d0a=1764311728,1764643482,1764662823,1764742943; Hm_lpvt_25f5e7a3a5dbb293d7dd35d5f1be8d0a=1764742943; HMACCOUNT=55F2182717FD6AE6', +} + +params = { + 'pageSize': '10', + 'pageNo': '1', +} + +response = requests.get( + 'https://yunxiu.f6car.cn/member/customer/listForPermission', + params=params, + cookies=cookies, + headers=headers, +) + +print(response.json()) \ No newline at end of file