From 90656ab36a148b1c9757168e391f16f750b0aff2 Mon Sep 17 00:00:00 2001 From: wanjia Date: Tue, 15 Apr 2025 16:48:19 +0800 Subject: [PATCH] =?UTF-8?q?=E9=80=9A=E8=BF=87=E9=A3=9E=E4=B9=A6=E5=90=8C?= =?UTF-8?q?=E6=AD=A5=E6=95=B0=E6=8D=AE=E5=BA=93=E5=88=9B=E5=BB=BA=E8=BE=BE?= =?UTF-8?q?=E4=BA=BA=E7=9F=A5=E8=AF=86=E5=BA=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- feishu/README.md | 178 +++ feishu/examples/creator_kb_query.sql | 36 + feishu/examples/sample_data_template.txt | 42 + feishu/feishu.py | 886 ++++++++++++- requirements.txt | Bin 1636 -> 4584 bytes role_based_system/urls.py | 97 +- user_management/gmail_integration.py | 372 +++++- ...mail_email_conversationsummary_and_more.py | 50 + user_management/models.py | 38 +- user_management/urls.py | 12 +- user_management/views.py | 1167 +++++++++++++++-- 11 files changed, 2626 insertions(+), 252 deletions(-) create mode 100644 feishu/README.md create mode 100644 feishu/examples/creator_kb_query.sql create mode 100644 feishu/examples/sample_data_template.txt create mode 100644 user_management/migrations/0009_gmailcredential_gmail_email_conversationsummary_and_more.py diff --git a/feishu/README.md b/feishu/README.md new file mode 100644 index 0000000..fe780a3 --- /dev/null +++ b/feishu/README.md @@ -0,0 +1,178 @@ +# 飞书数据同步工具 + +这个工具提供了与飞书多维表格进行数据同步的功能,支持双向同步:从飞书导入数据到数据库,以及从数据库导出数据到飞书。同时支持将达人数据同步到外部知识库。 + +## 功能特点 + +- 从飞书多维表格导入数据到数据库 +- 从Excel文件更新数据库记录 +- 从数据库导出数据到飞书多维表格 +- 从特定数据库模型导入数据到飞书 +- 从自定义SQL查询导入数据到飞书 +- 将达人信息同步到外部知识库 +- 支持Gmail和飞书达人信息的统一管理 +- 支持自定义字段映射 +- 支持记录过滤和数量限制 + +## 使用方法 + +### 显示帮助信息 + +```bash +python feishu.py help +``` + +### 从飞书导入数据到数据库 + +```bash +python feishu.py sync_from_feishu +``` + +### 从Excel文件更新数据库记录 + +```bash +python feishu.py update_from_excel +``` + +### 从数据库导出数据到飞书多维表格 + +```bash +python feishu.py export_to_feishu [--handle 关键字] [--limit 数量] +``` + +例如,导出Handle包含"tiktok"的前10条记录: + +```bash +python feishu.py export_to_feishu --handle tiktok --limit 10 +``` + +### 从特定数据库模型导入数据到飞书 + +```bash +python feishu.py import_from_db <模型名称> [--filters 字段1=值1,字段2=值2,...] [--limit 数量] +``` + +支持的模型: +- Data:数据模型 +- KnowledgeBase:知识库模型 +- FeishuCreator:飞书创作者模型 + +例如,导入技术部的管理员数据: + +```bash +python feishu.py import_from_db Data --filters type=admin,department=技术部 --limit 50 +``` + +### 从自定义SQL查询导入数据到飞书 + +```bash +python feishu.py custom_import +``` + +SQL文件格式示例: + +```sql +-- 查询语句 +SELECT id, name, type FROM your_table WHERE condition; + +-- FIELD_MAPPING: +{ + "id": "飞书ID字段", + "name": "飞书名称字段", + "type": "飞书类型字段" +} +``` + +### 将达人信息同步到知识库 + +```bash +python feishu.py sync_to_kb --id | --handle | --email +``` + +例如,同步特定达人信息到知识库: + +```bash +python feishu.py sync_to_kb --handle tiktok_user123 +``` + +## 示例 + +在`examples`目录中提供了几个示例文件: + +- `custom_query.sql`:自定义SQL查询示例,包含字段映射 +- `creator_kb_query.sql`:查询达人信息并检查知识库映射的SQL示例 +- `sample_data_template.txt`:Excel导入模板说明 + +## REST API接口 + +除了命令行工具外,还提供了以下REST API接口: + +### 1. 飞书数据同步API + +``` +POST /api/feishu/sync +``` + +参数: +- `sync_type` (可选):同步类型,默认为"all" +- `handle` (可选):指定特定的达人Handle +- `create_kb` (可选):是否创建知识库,默认为false + +### 2. 达人信息同步到知识库API + +``` +POST /api/feishu/to_kb +``` + +参数: +- `creator_id`、`handle`或`email`:至少提供一个参数用于识别达人 + +### 3. 检查达人知识库API + +``` +POST /api/feishu/check_kb +``` + +参数: +- `creator_id`、`handle`或`email`:至少提供一个参数用于识别达人 + +## 与Gmail集成 + +本工具支持与Gmail系统集成,实现达人信息的统一管理: + +1. 当通过飞书同步达人信息到知识库时,会检查该达人是否已有Gmail映射 +2. 如果该达人邮箱已有Gmail映射,则使用现有知识库 +3. 如果没有映射,则创建新的知识库并建立映射关系 +4. 知识库文档采用追加模式,保留历史记录 + +## 配置 + +飞书API配置位于代码中,包括: + +- APP_TOKEN:飞书应用的AppToken +- TABLE_ID:飞书多维表格的TableID +- USER_ACCESS_TOKEN:用户访问令牌 + +如需修改这些配置,请编辑相应的函数中的变量。 + +## 注意事项 + +1. 确保已安装所需的依赖:`lark_oapi`, `pandas`, `django`等 +2. 在执行数据库操作前,确保数据库连接已正确配置 +3. 自定义SQL查询应避免执行修改数据的操作(INSERT, UPDATE, DELETE等) +4. 导入大量数据时,可能需要分批处理,使用`--limit`参数控制数量 +5. 创建知识库需要管理员权限,确保有可用的管理员账号 + +## 常见问题 + +### Q: 导入数据时出现字段不匹配错误? + +A: 确保字段名称与飞书多维表格中的字段名称完全匹配,或使用字段映射功能。 + +### Q: 如何添加新的数据模型支持? + +A: 在`import_from_database_to_feishu`函数中添加新的模型映射,并按照现有模式实现字段映射逻辑。 + +### Q: 如何处理Gmail和飞书同一个达人的数据合并? + +A: 系统会自动检查达人邮箱,如果在Gmail中已有映射,会使用同一个知识库,避免重复创建。 \ No newline at end of file diff --git a/feishu/examples/creator_kb_query.sql b/feishu/examples/creator_kb_query.sql new file mode 100644 index 0000000..d11f6bc --- /dev/null +++ b/feishu/examples/creator_kb_query.sql @@ -0,0 +1,36 @@ +-- 查询所有有邮箱的达人,检查是否已有知识库映射 +SELECT + fc.id as creator_id, + fc.handle as handle, + fc.email as email, + fc.contact_person as contact_person, + fc.fans_count as fans_count, + fc.tiktok_url as tiktok_url, + gtm.knowledge_base_id as kb_id, + kb.name as kb_name, + kb.document_count as doc_count +FROM + feishu_creators fc +LEFT JOIN + gmail_talent_mappings gtm ON fc.email = gtm.talent_email AND gtm.is_active = 1 +LEFT JOIN + knowledge_bases kb ON gtm.knowledge_base_id = kb.id +WHERE + fc.email IS NOT NULL AND fc.email != '' +ORDER BY + fc.updated_at DESC +LIMIT + 50; + +-- FIELD_MAPPING: +{ + "creator_id": "达人ID", + "handle": "达人Handle", + "email": "达人邮箱", + "contact_person": "对接人", + "fans_count": "粉丝数", + "tiktok_url": "链接", + "kb_id": "知识库ID", + "kb_name": "知识库名称", + "doc_count": "文档数量" +} \ No newline at end of file diff --git a/feishu/examples/sample_data_template.txt b/feishu/examples/sample_data_template.txt new file mode 100644 index 0000000..b45746d --- /dev/null +++ b/feishu/examples/sample_data_template.txt @@ -0,0 +1,42 @@ +# Excel文件模板说明 + +您可以使用Excel文件来批量更新数据库中的记录。Excel文件应包含以下列: + +## 必需字段 + +- `Handle`:用于匹配数据库中的记录,此字段必填 + +## 可选字段(仅更新不为空的字段) + +- `contact_person`:对接人 +- `tiktok_url`:链接 +- `fans_count`:粉丝数 +- `gmv`:GMV +- `email`:邮箱 +- `phone`:手机号|WhatsApp +- `account_type`:账号属性 +- `price_quote`:报价 +- `response_speed`:回复速度 +- `cooperation_intention`:合作意向 +- `payment_method`:支付方式 +- `payment_account`:收款账号 +- `address`:收件地址 +- `has_ooin`:签约OOIN? +- `source`:渠道来源 +- `contact_status`:建联进度 +- `system_categories`:系统展示的带货品类 +- `actual_categories`:实际高播放量带货品类 +- `human_categories`:达人标想要货品类 + +## 示例 + +| Handle | contact_person | fans_count | email | phone | +|------------|----------------|------------|---------------------|------------| +| user1 | 张三 | 10000 | user1@example.com | 13800000001| +| user2 | 李四 | 20000 | user2@example.com | 13800000002| + +## 注意事项 + +1. 导入时会根据Handle匹配记录,如果找不到匹配的记录则会跳过 +2. 只有非空字段会被更新,空字段会保留原值 +3. 确保Excel文件保存为.xlsx格式 \ No newline at end of file diff --git a/feishu/feishu.py b/feishu/feishu.py index ecfc8de..68a4036 100644 --- a/feishu/feishu.py +++ b/feishu/feishu.py @@ -3,7 +3,14 @@ import django import os import sys import pandas as pd +import requests from django.db import transaction +from django.db.models import Q +from django.db import connection +from datetime import datetime +import traceback +import tempfile +import uuid # 设置 Django 环境 sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) @@ -12,8 +19,12 @@ django.setup() import lark_oapi as lark from lark_oapi.api.bitable.v1 import * -from user_management.models import FeishuCreator +from user_management.models import FeishuCreator, Data, KnowledgeBase, KnowledgeBaseDocument +from django.conf import settings +import logging +from django.contrib.auth import get_user_model +logger = logging.getLogger(__name__) # SDK 使用说明: https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/server-side-sdk/python--sdk/preparations-before-development # 以下示例代码默认根据文档示例值填充,如果存在代码问题,请在 API 调试台填上相关必要参数后再复制代码使用 @@ -217,8 +228,128 @@ def update_from_excel(excel_file_path): except Exception as e: print(f"处理Excel文件时出错: {str(e)}") -def sync_from_feishu(): - """从飞书多维表格同步数据""" +def sync_from_feishu(app_token=None, table_id=None, user_access_token=None, filters=None): + """从飞书同步数据到系统 + + 参数: + app_token: 飞书应用的APP_TOKEN,如不提供则使用默认值 + table_id: 飞书多维表格的TABLE_ID,如不提供则使用默认值 + user_access_token: 用户访问令牌,如不提供则使用默认值 + filters: 筛选条件,字典格式 + + 返回: + dict: 包含同步统计信息的字典 + """ + try: + # 使用传入的参数或默认值 + app_token = app_token or settings.FEISHU_APP_TOKEN + table_id = table_id or settings.FEISHU_TABLE_ID + user_access_token = user_access_token or settings.FEISHU_USER_ACCESS_TOKEN + + if not all([app_token, table_id, user_access_token]): + logger.error("飞书API凭据不完整") + return { + 'created': 0, + 'updated': 0, + 'errors': 1, + 'total': 0, + 'error_message': '飞书API凭据不完整' + } + + # 初始化统计信息 + created_count = 0 + updated_count = 0 + error_count = 0 + created_creators = [] + + # 获取飞书数据 + client = lark.Client.builder() \ + .enable_set_token(True) \ + .log_level(lark.LogLevel.DEBUG) \ + .build() + + print("开始从飞书同步数据...") + all_records = fetch_all_records(client, app_token, table_id, user_access_token) + + if not all_records: + print("未获取到任何记录") + return { + 'created': 0, + 'updated': 0, + 'errors': 0, + 'total': 0 + } + + print("\n开始更新数据库...") + + for record in all_records: + creator, created = save_to_database(record) + if creator: + if created: + created_count += 1 + if created_count % 10 == 0: + print(f"已创建 {created_count} 条记录...") + created_creators.append(creator) + else: + updated_count += 1 + if updated_count % 10 == 0: + print(f"已更新 {updated_count} 条记录...") + else: + error_count += 1 + print(f"处理记录失败") + + print("\n飞书同步完成!统计信息:") + print(f"新建记录:{created_count}") + print(f"更新记录:{updated_count}") + print(f"错误记录:{error_count}") + print(f"总记录数:{len(all_records)}") + + # 返回统计信息,增加了created_creators列表 + return { + 'created': created_count, + 'updated': updated_count, + 'errors': error_count, + 'total': created_count + updated_count, + 'created_creators': created_creators # 新增:返回新创建的记录对象列表 + } + + except Exception as e: + logger.error(f"从飞书同步数据时出错: {str(e)}") + logger.error(traceback.format_exc()) + return { + 'created': 0, + 'updated': 0, + 'errors': 1, + 'total': 0, + 'error_message': str(e) + } + +def format_field_value(value, field_type='text'): + """格式化字段值以符合飞书多维表格API要求""" + if value is None or value == '': + return None + + if field_type == 'text': + return {"text": str(value)} + elif field_type == 'number': + try: + return float(value) + except (ValueError, TypeError): + return None + elif field_type == 'checkbox': + return value.lower() in ('true', 'yes', '是', '1') + elif field_type == 'multi_select': + # 多选项需要返回一个选项ID列表 + if isinstance(value, str): + items = [item.strip() for item in value.split(',') if item.strip()] + return [{"text": item} for item in items] + elif isinstance(value, list): + return [{"text": str(item)} for item in value] + return [] + return {"text": str(value)} + +def export_to_feishu(query_set=None): + """从数据库导出记录到飞书多维表格""" # 创建client client = lark.Client.builder() \ .enable_set_token(True) \ @@ -230,61 +361,740 @@ def sync_from_feishu(): TABLE_ID = "tbl3oikG3F8YYtVA" USER_ACCESS_TOKEN = "u-ecM5BmzKx4uHz3sG0FouQSk1l9kxgl_3Xa00l5Ma24Jy" - print("开始从飞书同步数据...") - all_records = fetch_all_records(client, APP_TOKEN, TABLE_ID, USER_ACCESS_TOKEN) - - if not all_records: - print("未获取到任何记录") + print("开始从数据库导出数据到飞书...") + + # 如果没有提供查询集,则使用所有没有record_id的记录 + if not query_set: + query_set = FeishuCreator.objects.filter(record_id='') + + if not query_set.exists(): + print("没有需要导出的记录") return - - print("\n开始更新数据库...") + created_count = 0 updated_count = 0 error_count = 0 - - for record in all_records: - creator, created = save_to_database(record) - if creator: - if created: - created_count += 1 - if created_count % 10 == 0: - print(f"已创建 {created_count} 条记录...") + + for creator in query_set: + try: + # 准备字段数据 + fields = { + "对接人": format_field_value(creator.contact_person), + "Handle": format_field_value(creator.handle), + "链接": format_field_value(creator.tiktok_url), + "粉丝数": format_field_value(creator.fans_count), + "GMV": format_field_value(creator.gmv), + "邮箱": format_field_value(creator.email), + "手机号|WhatsApp": format_field_value(creator.phone), + "账号属性": format_field_value(creator.account_type, 'multi_select'), + "报价": format_field_value(creator.price_quote), + "回复速度": format_field_value(creator.response_speed), + "合作意向": format_field_value(creator.cooperation_intention), + "支付方式": format_field_value(creator.payment_method), + "收款账号": format_field_value(creator.payment_account), + "收件地址": format_field_value(creator.address), + "签约OOIN?": format_field_value(creator.has_ooin), + "渠道来源": format_field_value(creator.source), + "建联进度": format_field_value(creator.contact_status), + "合作品牌": creator.cooperation_brands, + "系统展示的带货品类": format_field_value(creator.system_categories, 'multi_select'), + "实际高播放量带货品类": format_field_value(creator.actual_categories, 'multi_select'), + "达人标想要货品类": format_field_value(creator.human_categories) + } + + # 过滤掉None值 + fields = {k: v for k, v in fields.items() if v is not None} + + if creator.record_id: + # 更新现有记录 + request = UpdateAppTableRecordRequest.builder() \ + .app_token(APP_TOKEN) \ + .table_id(TABLE_ID) \ + .record_id(creator.record_id) \ + .request_body(UpdateAppTableRecordRequestBody.builder().fields(fields).build()) \ + .build() + + option = lark.RequestOption.builder().user_access_token(USER_ACCESS_TOKEN).build() + response = client.bitable.v1.app_table_record.update(request, option) + + if response.success(): + updated_count += 1 + print(f"已更新记录: {creator.handle}") + else: + error_count += 1 + print(f"更新失败: {creator.handle}, 错误: {response.msg}") else: - updated_count += 1 - if updated_count % 10 == 0: - print(f"已更新 {updated_count} 条记录...") - else: + # 创建新记录 + request = CreateAppTableRecordRequest.builder() \ + .app_token(APP_TOKEN) \ + .table_id(TABLE_ID) \ + .request_body(CreateAppTableRecordRequestBody.builder().fields(fields).build()) \ + .build() + + option = lark.RequestOption.builder().user_access_token(USER_ACCESS_TOKEN).build() + response = client.bitable.v1.app_table_record.create(request, option) + + if response.success(): + # 获取新记录的ID并保存 + record_id = response.data.record_id + creator.record_id = record_id + creator.save() + + created_count += 1 + print(f"已创建记录: {creator.handle}, ID: {record_id}") + else: + error_count += 1 + print(f"创建失败: {creator.handle}, 错误: {response.msg}") + + except Exception as e: error_count += 1 - print(f"处理记录失败") - - print("\n飞书同步完成!统计信息:") + print(f"处理记录 {creator.handle} 时出错: {str(e)}") + import traceback + print(traceback.format_exc()) + + print("\n飞书导出完成!统计信息:") print(f"新建记录:{created_count}") print(f"更新记录:{updated_count}") print(f"错误记录:{error_count}") - print(f"总记录数:{len(all_records)}") + print(f"总处理记录数:{created_count + updated_count + error_count}") + +def import_from_database_to_feishu(model_name, filters=None, limit=None): + """从指定数据库模型导入数据到飞书多维表格 + + 参数: + model_name (str): 模型名称,如 'Data', 'KnowledgeBase' 等 + filters (dict): 过滤条件,如 {'department': '技术部'} + limit (int): 限制导入的记录数量 + """ + # 创建client + client = lark.Client.builder() \ + .enable_set_token(True) \ + .log_level(lark.LogLevel.DEBUG) \ + .build() + + # 配置参数 + APP_TOKEN = "XYE6bMQUOaZ5y5svj4vcWohGnmg" + TABLE_ID = "tbl3oikG3F8YYtVA" + USER_ACCESS_TOKEN = "u-ecM5BmzKx4uHz3sG0FouQSk1l9kxgl_3Xa00l5Ma24Jy" + + print(f"开始从{model_name}模型导入数据到飞书...") + + # 选择模型 + model_mapping = { + 'Data': Data, + 'KnowledgeBase': KnowledgeBase, + 'FeishuCreator': FeishuCreator, + } + + model_class = model_mapping.get(model_name) + if not model_class: + print(f"错误: 不支持的模型名称 {model_name}") + return + + # 构建查询 + query = model_class.objects.all() + + # 应用过滤条件 + if filters: + q_objects = Q() + for field, value in filters.items(): + if isinstance(value, list): + # 对列表值使用OR查询 + q_filter = Q() + for v in value: + q_filter |= Q(**{field: v}) + q_objects &= q_filter + else: + q_objects &= Q(**{field: value}) + query = query.filter(q_objects) + + # 限制记录数量 + if limit and isinstance(limit, int) and limit > 0: + query = query[:limit] + + # 检查查询结果 + total_count = query.count() + if total_count == 0: + print("没有符合条件的记录") + return + + print(f"找到 {total_count} 条记录,准备导入到飞书...") + + # 记录统计 + created_count = 0 + error_count = 0 + + # 处理不同模型的字段映射 + for index, record in enumerate(query): + try: + if model_name == 'Data': + # Data模型到飞书字段的映射 + fields = { + "名称": format_field_value(record.name), + "描述": format_field_value(record.desc), + "类型": format_field_value(record.get_type_display()), + "部门": format_field_value(record.department), + "字符长度": format_field_value(record.char_length, 'number'), + "文档数量": format_field_value(record.document_count, 'number'), + "创建时间": format_field_value(record.create_time.strftime('%Y-%m-%d %H:%M:%S')), + "更新时间": format_field_value(record.update_time.strftime('%Y-%m-%d %H:%M:%S')), + "ID": format_field_value(str(record.id)), + } + elif model_name == 'KnowledgeBase': + # KnowledgeBase模型到飞书字段的映射 + fields = { + "名称": format_field_value(record.name), + "描述": format_field_value(record.desc), + "类型": format_field_value(record.type), + "部门": format_field_value(record.department), + "小组": format_field_value(record.group), + "文档数量": format_field_value(record.document_count, 'number'), + "字符长度": format_field_value(record.char_length, 'number'), + "创建时间": format_field_value(record.create_time.strftime('%Y-%m-%d %H:%M:%S')), + "ID": format_field_value(str(record.id)), + } + else: + # 对于FeishuCreator模型,使用export_to_feishu函数的逻辑 + continue + + # 过滤掉None值 + fields = {k: v for k, v in fields.items() if v is not None} + + # 创建新记录 + request = CreateAppTableRecordRequest.builder() \ + .app_token(APP_TOKEN) \ + .table_id(TABLE_ID) \ + .request_body(CreateAppTableRecordRequestBody.builder().fields(fields).build()) \ + .build() + + option = lark.RequestOption.builder().user_access_token(USER_ACCESS_TOKEN).build() + response = client.bitable.v1.app_table_record.create(request, option) + + if response.success(): + created_count += 1 + if created_count % 10 == 0 or created_count == total_count: + print(f"已导入 {created_count}/{total_count} 条记录...") + else: + error_count += 1 + print(f"导入第 {index+1} 条记录失败,错误: {response.msg}") + + except Exception as e: + error_count += 1 + print(f"处理第 {index+1} 条记录时出错: {str(e)}") + + print("\n飞书导入完成!统计信息:") + print(f"成功导入记录数:{created_count}") + print(f"失败记录数:{error_count}") + print(f"总记录数:{total_count}") + +def customize_import_to_feishu(sql_query, field_mapping=None): + """从自定义SQL查询结果导入数据到飞书多维表格 + + 参数: + sql_query (str): SQL查询语句 + field_mapping (dict): 字段映射,如 {'db_field': 'feishu_field'} + """ + # 创建client + client = lark.Client.builder() \ + .enable_set_token(True) \ + .log_level(lark.LogLevel.DEBUG) \ + .build() + + # 配置参数 + APP_TOKEN = "XYE6bMQUOaZ5y5svj4vcWohGnmg" + TABLE_ID = "tbl3oikG3F8YYtVA" + USER_ACCESS_TOKEN = "u-ecM5BmzKx4uHz3sG0FouQSk1l9kxgl_3Xa00l5Ma24Jy" + + print("开始从自定义SQL查询导入数据到飞书...") + + try: + # 执行SQL查询 + with connection.cursor() as cursor: + cursor.execute(sql_query) + columns = [col[0] for col in cursor.description] + result_set = cursor.fetchall() + + # 检查查询结果 + if not result_set: + print("SQL查询没有返回任何结果") + return + + print(f"查询返回 {len(result_set)} 条记录,准备导入到飞书...") + + # 默认字段映射:数据库字段名 -> 飞书字段名 + if not field_mapping: + field_mapping = {col: col for col in columns} + + # 记录统计 + created_count = 0 + error_count = 0 + + # 处理每条记录 + for index, row in enumerate(result_set): + try: + # 创建记录字典 + record_dict = dict(zip(columns, row)) + + # 将数据映射到飞书字段 + fields = {} + for db_field, feishu_field in field_mapping.items(): + if db_field in record_dict: + value = record_dict[db_field] + # 处理不同类型的值 + if isinstance(value, (int, float)): + fields[feishu_field] = format_field_value(value, 'number') + else: + fields[feishu_field] = format_field_value(value) + + # 过滤掉None值 + fields = {k: v for k, v in fields.items() if v is not None} + + # 创建新记录 + request = CreateAppTableRecordRequest.builder() \ + .app_token(APP_TOKEN) \ + .table_id(TABLE_ID) \ + .request_body(CreateAppTableRecordRequestBody.builder().fields(fields).build()) \ + .build() + + option = lark.RequestOption.builder().user_access_token(USER_ACCESS_TOKEN).build() + response = client.bitable.v1.app_table_record.create(request, option) + + if response.success(): + created_count += 1 + if created_count % 10 == 0 or created_count == len(result_set): + print(f"已导入 {created_count}/{len(result_set)} 条记录...") + else: + error_count += 1 + print(f"导入第 {index+1} 条记录失败,错误: {response.msg}") + + except Exception as e: + error_count += 1 + print(f"处理第 {index+1} 条记录时出错: {str(e)}") + import traceback + print(traceback.format_exc()) + + print("\n自定义SQL导入完成!统计信息:") + print(f"成功导入记录数:{created_count}") + print(f"失败记录数:{error_count}") + print(f"总记录数:{len(result_set)}") + + except Exception as e: + print(f"执行SQL查询时出错: {str(e)}") + import traceback + print(traceback.format_exc()) + +def print_help(): + """打印帮助信息""" + print("飞书数据同步工具") + print("================") + print("\n可用命令:") + print(" 1. 从飞书导入数据到数据库:") + print(" python feishu.py sync_from_feishu") + print() + print(" 2. 从Excel文件更新数据库记录:") + print(" python feishu.py update_from_excel ") + print() + print(" 3. 从数据库导出数据到飞书多维表格:") + print(" python feishu.py export_to_feishu [--handle 关键字] [--limit 数量]") + print() + print(" 4. 从特定数据库模型导入数据到飞书:") + print(" python feishu.py import_from_db <模型名称> [--filters 字段1=值1,字段2=值2,...] [--limit 数量]") + print(" 支持的模型: Data, KnowledgeBase, FeishuCreator") + print(" 示例: python feishu.py import_from_db Data --filters type=admin,department=技术部 --limit 50") + print() + print(" 5. 从自定义SQL查询导入数据到飞书:") + print(" python feishu.py custom_import ") + print(" SQL文件中应包含SQL查询语句,及可选的字段映射JSON") + print() + print(" 6. 将达人信息同步到知识库:") + print(" python feishu.py sync_to_kb --id | --handle | --email ") + print(" 示例: python feishu.py sync_to_kb --handle tiktok_user123") + print() + print(" 7. 显示帮助信息:") + print(" python feishu.py help") + print() + +def sync_to_knowledge_base(creator_id=None, handle=None, email=None): + """将达人信息同步到外部知识库 + + 参数: + creator_id: 达人ID + handle: 达人Handle + email: 达人Email + + 返回: + tuple: (KnowledgeBase对象, 是否新创建) + """ + try: + import json + import requests + import tempfile + import os + from datetime import datetime + from django.conf import settings + from user_management.models import FeishuCreator, KnowledgeBase, User, KnowledgeBaseDocument + + # 查找达人 + query = FeishuCreator.objects.all() + if creator_id: + query = query.filter(id=creator_id) + elif handle: + query = query.filter(handle=handle) + elif email: + query = query.filter(email=email) + + creator = query.first() + if not creator: + logger.error(f"未找到达人: creator_id={creator_id}, handle={handle}, email={email}") + return None, False + + # 检查达人是否有足够信息 + if not creator.handle: + logger.error(f"达人没有handle,无法创建知识库: {creator.id}") + return None, False + + # 检查是否已有通过Gmail映射的知识库 + gmail_kb = None + if creator.email: + from user_management.models import GmailTalentMapping + gmail_mapping = GmailTalentMapping.objects.filter( + talent_email=creator.email, + is_active=True + ).first() + + if gmail_mapping and gmail_mapping.knowledge_base: + gmail_kb = gmail_mapping.knowledge_base + logger.info(f"达人 {creator.handle} 已有Gmail知识库: {gmail_kb.id}") + return gmail_kb, False + + # 获取当前用户(组长) + User = get_user_model() + admin_user = User.objects.filter(role='leader').first() + + if not admin_user: + logger.error("未找到组长用户,无法创建知识库") + return None, False + + # 生成知识库名称 + kb_name = f"达人-{creator.handle}" + + # 检查是否已存在同名知识库 + existed_kb = KnowledgeBase.objects.filter(name=kb_name).first() + if existed_kb: + logger.info(f"知识库 {kb_name} 已存在,ID: {existed_kb.id}") + return existed_kb, False + + # 创建新知识库 + kb = KnowledgeBase.objects.create( + name=kb_name, + desc=f"达人 {creator.handle} 的信息知识库", + type='private', # 设置为私有知识库 + user_id=admin_user.id, + department=admin_user.department, + document_count=0 + ) + + logger.info(f"已创建知识库: {kb.id}") + + # 调用内部方法创建外部知识库 + from user_management.views import KnowledgeBaseViewSet + kb_viewset = KnowledgeBaseViewSet() + + try: + external_id = kb_viewset._create_external_dataset(kb) + # 保存external_id + kb.external_id = external_id + kb.save() + logger.info(f"已创建外部知识库,ID: {external_id}") + except Exception as e: + logger.error(f"创建外部知识库失败: {str(e)}") + # 如果外部知识库创建失败,删除本地知识库 + kb.delete() + return None, False + + # 准备达人信息文档 + content = [] + + # 基本信息 + content.append("# 达人基本信息") + content.append(f"Handle: {creator.handle}") + content.append(f"邮箱: {creator.email}") + content.append(f"对接人: {creator.contact_person}") + content.append(f"链接: {creator.tiktok_url}") + content.append(f"粉丝数: {creator.fans_count}") + content.append(f"GMV: {creator.gmv}") + content.append(f"电话: {creator.phone}") + + # 账号属性 + content.append("\n# 账号属性") + content.append(f"账号属性: {creator.account_type}") + content.append(f"报价: {creator.price_quote}") + content.append(f"回复速度: {creator.response_speed}") + + # 合作信息 + content.append("\n# 合作信息") + content.append(f"合作意向: {creator.cooperation_intention}") + content.append(f"支付方式: {creator.payment_method}") + content.append(f"收款账号: {creator.payment_account}") + content.append(f"收件地址: {creator.address}") + content.append(f"签约OOIN?: {creator.has_ooin}") + + # 渠道和品类 + content.append("\n# 渠道和品类") + content.append(f"渠道来源: {creator.source}") + content.append(f"建联进度: {creator.contact_status}") + content.append(f"合作品牌: {creator.cooperation_brands}") + content.append(f"系统展示品类: {creator.system_categories}") + content.append(f"实际品类: {creator.actual_categories}") + content.append(f"达人想要品类: {creator.human_categories}") + + # 其他信息 + content.append("\n# 其他信息") + content.append(f"达人base: {creator.creator_base}") + content.append(f"备注: {creator.notes}") + content.append(f"更新时间: {creator.updated_at.strftime('%Y-%m-%d %H:%M:%S')}") + + # 合并内容 + full_content = "\n".join(content) + + # 创建临时文件 + with tempfile.NamedTemporaryFile(delete=False, suffix='.txt', mode='w', encoding='utf-8') as temp: + temp.write(full_content) + temp_file_path = temp.name + + try: + # 准备文档上传需要的段落数据 + doc_data = { + "name": f"达人信息-{creator.handle}.txt", + "paragraphs": [ + { + "title": "达人基本资料", + "content": full_content, + "is_active": True, + "problem_list": [] + } + ] + } + + # 调用上传API + upload_response = kb_viewset._call_upload_api(kb.external_id, doc_data) + + if upload_response and upload_response.get('code') == 200 and upload_response.get('data'): + # 上传成功,保存记录到数据库 + document_id = upload_response['data']['id'] + doc_record = KnowledgeBaseDocument.objects.create( + knowledge_base=kb, + document_id=document_id, + document_name=f"达人信息-{creator.handle}.txt", + external_id=document_id, + status='active' + ) + + logger.info(f"文档上传成功,ID: {document_id}") + + # 更新知识库文档计数 + kb.document_count = 1 + kb.save() + + # 如果有邮箱,创建Gmail映射 + if creator.email: + from user_management.models import GmailTalentMapping + GmailTalentMapping.objects.create( + user=admin_user, + talent_email=creator.email, + knowledge_base=kb, + conversation_id=f"feishu_{creator.id}", + is_active=True + ) + logger.info(f"已创建Gmail映射: {creator.email} -> {kb.id}") + + logger.info(f"成功为达人 {creator.handle} 创建知识库并上传文档: {kb.id}") + return kb, True + else: + # 上传失败,记录错误信息 + error_msg = upload_response.get('message', '未知错误') if upload_response else '上传API调用失败' + logger.error(f"文档上传失败: {error_msg}") + + # 尝试删除创建的知识库和外部知识库 + try: + kb_viewset._delete_external_dataset(kb.external_id) + except Exception as del_err: + logger.error(f"删除外部知识库失败: {str(del_err)}") + + kb.delete() + return None, False + + except Exception as e: + logger.error(f"文档上传过程中出错: {str(e)}") + import traceback + logger.error(traceback.format_exc()) + + # 尝试删除创建的知识库和外部知识库 + try: + kb_viewset._delete_external_dataset(kb.external_id) + except Exception as del_err: + logger.error(f"删除外部知识库失败: {str(del_err)}") + + kb.delete() + return None, False + finally: + # 清理临时文件 + try: + os.unlink(temp_file_path) + except: + pass + + except Exception as e: + logger.error(f"同步达人到知识库时出错: {str(e)}") + import traceback + logger.error(traceback.format_exc()) + return None, False def main(): """主函数""" - if len(sys.argv) < 2: - print("使用方法:") - print("1. 从飞书同步: python feishu.py sync") - print("2. 从Excel更新: python feishu.py excel ") + if len(sys.argv) < 2 or sys.argv[1] == "help": + print_help() return - + command = sys.argv[1] - if command == 'sync': + if command == "sync_from_feishu": sync_from_feishu() - elif command == 'excel': - if len(sys.argv) != 3: - print("使用方法: python feishu.py excel ") + elif command == "update_from_excel": + if len(sys.argv) < 3: + print("错误: 未指定Excel文件路径") + print("用法: python feishu.py update_from_excel ") return excel_file_path = sys.argv[2] update_from_excel(excel_file_path) + elif command == "export_to_feishu": + # 可选参数处理 + handle_filter = None + limit = None + + i = 2 + while i < len(sys.argv): + if sys.argv[i] == "--handle" and i + 1 < len(sys.argv): + handle_filter = sys.argv[i+1] + i += 2 + elif sys.argv[i] == "--limit" and i + 1 < len(sys.argv): + try: + limit = int(sys.argv[i+1]) + except ValueError: + print(f"警告: 无效的limit值 '{sys.argv[i+1]}',将使用默认值") + i += 2 + else: + i += 1 + + # 如果提供了handle过滤器,创建自定义查询集 + if handle_filter: + query_set = FeishuCreator.objects.filter(handle__icontains=handle_filter) + if limit: + query_set = query_set[:limit] + export_to_feishu(query_set) + else: + export_to_feishu() + elif command == "import_from_db": + if len(sys.argv) < 3: + print("错误: 未指定数据库模型") + print("用法: python feishu.py import_from_db <模型名称> [--filters 字段1=值1,字段2=值2,...] [--limit 数量]") + print("支持的模型: Data, KnowledgeBase, FeishuCreator") + return + + model_name = sys.argv[2] + filters = {} + limit = None + + # 解析额外参数 + i = 3 + while i < len(sys.argv): + if sys.argv[i] == "--filters" and i + 1 < len(sys.argv): + filter_str = sys.argv[i+1] + for item in filter_str.split(','): + if '=' in item: + key, value = item.split('=', 1) + filters[key.strip()] = value.strip() + i += 2 + elif sys.argv[i] == "--limit" and i + 1 < len(sys.argv): + try: + limit = int(sys.argv[i+1]) + except ValueError: + print(f"警告: 无效的limit值 '{sys.argv[i+1]}',将使用默认值") + i += 2 + else: + i += 1 + + import_from_database_to_feishu(model_name, filters, limit) + elif command == "custom_import": + if len(sys.argv) < 3: + print("错误: 未指定SQL文件路径") + print("用法: python feishu.py custom_import ") + return + + sql_file_path = sys.argv[2] + + # 读取SQL文件 + try: + with open(sql_file_path, 'r', encoding='utf-8') as f: + file_content = f.read().strip() + + # 判断是否包含字段映射 + if '-- FIELD_MAPPING:' in file_content: + sql_part, mapping_part = file_content.split('-- FIELD_MAPPING:', 1) + sql_query = sql_part.strip() + + try: + field_mapping = json.loads(mapping_part.strip()) + print(f"使用自定义字段映射: {field_mapping}") + except json.JSONDecodeError: + print(f"警告: 字段映射格式无效,将使用默认映射") + field_mapping = None + else: + sql_query = file_content + field_mapping = None + + # 执行导入 + customize_import_to_feishu(sql_query, field_mapping) + + except FileNotFoundError: + print(f"错误: 找不到文件 '{sql_file_path}'") + except Exception as e: + print(f"处理SQL文件时出错: {str(e)}") + elif command == "sync_to_kb": + # 同步达人信息到知识库 + if len(sys.argv) < 3: + print("错误: 未指定达人识别信息") + print("用法: python feishu.py sync_to_kb --id | --handle | --email ") + return + + creator_id = None + handle = None + email = None + + i = 2 + while i < len(sys.argv): + if sys.argv[i] == "--id" and i + 1 < len(sys.argv): + creator_id = sys.argv[i+1] + i += 2 + elif sys.argv[i] == "--handle" and i + 1 < len(sys.argv): + handle = sys.argv[i+1] + i += 2 + elif sys.argv[i] == "--email" and i + 1 < len(sys.argv): + email = sys.argv[i+1] + i += 2 + else: + i += 1 + + kb, created = sync_to_knowledge_base(creator_id, handle, email) + if kb: + status = "新创建" if created else "已存在" + print(f"达人知识库{status}: {kb.id} ({kb.name})") + print(f"文档数量: {kb.document_count}") + else: + print("同步失败,未能创建或找到达人知识库") else: - print("无效的命令。使用方法:") - print("1. 从飞书同步: python feishu.py sync") - print("2. 从Excel更新: python feishu.py excel ") + print(f"未知命令: {command}") + print_help() if __name__ == "__main__": main() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 2e65c21050df849b7e469929cc961e0502553e0a..05e6f06115e064a05fcbc28a2365a79f4cf876ed 100644 GIT binary patch literal 4584 zcma)=Pj6yJ5X9#kDIW#PfQ_?<94zf=l_-gJr6?y0V+^cq9t$`&eE1~wn|e(@W3Lri z?7!*huCA``9`@gV#${2OGB4w@($B6;>*rd3zt_*E{H&irIVmH3`sK&+gHC*7TeiBv z`LLe#>#t`Wo3*^%$l6{ww`F?S=yX<=H5#76X3%a0Gz$YPFtU+@_C`p z>A`+i(c$Y%|NfzKwo#--0n_aP^-nsFtTytzERz~zQ66O1lt;zG*Ln4mw&cD(u)`_H z0|mF50*7;XXzR1;5nI>yM&39j`#{C18RnFD6Ga;Up`4j`V$_VJK^zg9A=UHT@lgLuP2N55xD ztvpGIbS;FT5bnQ!vr(Ro=eyj5756c^?&fGt%r%qeq0Mnsm51_kPLHYrgC`VX_UTWe zxPPL5=Ze}t;AVmZ^0{()xdq~;GmG4F?oMT5;czbQ-enAB;u@9 z@1FGU4gn3UqvAyKEHADpGsi0CK3j?zJFkD@&D3bx;lo_#yXiqzGvQ`8SYJf3+URS1bs1+RdlI!hs9DAV0Zh>*%%{RrCVd>UY9Y?(v>oe*BCxhbH>0vqXO`M zFYa>g4r;yKpE;MWW#cz1Y8Y03VY919m%8dnMo0 z-6)JU=k4!8@u~b-z8u9*_I^{wDDty@erc`zs)!yUVz82$31x;ZvX6Q-Z{`xa8NEdA zp7kg4#;)DAJ&ON)alk2em*wwz?^s{h?pu$%+`($)lIvgPvi#cSfeuifPhW3g&oL^` z*-Rhr&w*}vZ&}^06*k|Lk>840nW1c>gO6=IQ;%OL)=04X`VO7J%bssQ%tP4oOv*bO zDT{wcy&r04qnx!kLuDm%m^ztn=*_zT zE#B2k?AYo8NtFYo?-%d}m${1au+;wJb10RJ$i^Z)<= delta 93 zcmV-j0HXisBjgMM|NfC!B$4c{lkfr1li~t!lcEFMlVAoSlL`lBlWYkhlOPHRlj;dv zlQ0V+lfnyZli&AlQ 20: + # 选取关键消息:第一条、最后几条以及中间的一些重要消息 + selected_messages = ( + conversation_history[:2] + # 前两条 + conversation_history[len(conversation_history)//2-2:len(conversation_history)//2+2] + # 中间四条 + conversation_history[-6:] # 最后六条 + ) + messages.extend(selected_messages) + else: + messages.extend(conversation_history) + + # 添加用户指令 + messages.append({ + "role": "user", + "content": "请根据以上对话历史生成一份全面的总结。" + }) + + # 构建API请求 + payload = { + "model": "deepseek-ai/DeepSeek-V3", + "messages": messages, + "stream": False, + "max_tokens": 1500, # 增加token上限以容纳完整总结 + "temperature": 0.3, # 降低随机性,使总结更加确定性 + "top_p": 0.9, + "top_k": 50, + "frequency_penalty": 0.5, + "presence_penalty": 0.2, + "n": 1, + "stop": [], + "response_format": { + "type": "text" + } + } + + headers = { + "Content-Type": "application/json", + "Authorization": f"Bearer {api_key}" + } + + logger.info(f"开始调用DeepSeek API生成对话总结") + response = requests.post(url, json=payload, headers=headers) + + if response.status_code != 200: + logger.error(f"DeepSeek API调用失败: {response.status_code}, {response.text}") + return None + + result = response.json() + logger.debug(f"DeepSeek API返回: {result}") + + # 提取回复内容 + if 'choices' in result and len(result['choices']) > 0: + summary = result['choices'][0]['message']['content'] + # 如果返回的内容为空,直接返回None + if not summary or summary.strip() == '': + logger.warning("DeepSeek API返回的总结内容为空") + return None + return summary + + logger.warning(f"DeepSeek API返回格式异常: {result}") + return None + + except Exception as e: + logger.error(f"调用DeepSeek API生成总结失败: {str(e)}") + logger.error(traceback.format_exc()) + return None diff --git a/user_management/migrations/0009_gmailcredential_gmail_email_conversationsummary_and_more.py b/user_management/migrations/0009_gmailcredential_gmail_email_conversationsummary_and_more.py new file mode 100644 index 0000000..ba748b8 --- /dev/null +++ b/user_management/migrations/0009_gmailcredential_gmail_email_conversationsummary_and_more.py @@ -0,0 +1,50 @@ +# Generated by Django 5.1.5 on 2025-04-14 04:27 + +import django.db.models.deletion +import uuid +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('user_management', '0008_gmailcredential_gmail_email_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='ConversationSummary', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('talent_email', models.EmailField(max_length=254, verbose_name='达人邮箱')), + ('conversation_id', models.CharField(max_length=100, verbose_name='对话ID')), + ('summary', models.TextField(verbose_name='对话总结')), + ('is_active', models.BooleanField(default=True, verbose_name='是否激活')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='conversation_summaries', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': '对话总结', + 'verbose_name_plural': '对话总结', + 'db_table': 'conversation_summaries', + }, + ), + migrations.CreateModel( + name='UserGoal', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('content', models.TextField(verbose_name='总目标内容')), + ('is_active', models.BooleanField(default=True, verbose_name='是否激活')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='goals', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': '用户总目标', + 'verbose_name_plural': '用户总目标', + 'db_table': 'user_goals', + }, + ), + ] diff --git a/user_management/models.py b/user_management/models.py index b7d43fa..f07c35b 100644 --- a/user_management/models.py +++ b/user_management/models.py @@ -765,4 +765,40 @@ class GmailAttachment(models.Model): verbose_name_plural = 'Gmail附件' def __str__(self): - return f"{self.filename} ({self.gmail_message_id})" + return f"{self.filename} ({self.filesize} bytes)" + +class UserGoal(models.Model): + """用户总目标模型""" + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='goals') + content = models.TextField(verbose_name='总目标内容') + is_active = models.BooleanField(default=True, verbose_name='是否激活') + created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + updated_at = models.DateTimeField(auto_now=True, verbose_name='更新时间') + + class Meta: + db_table = 'user_goals' + verbose_name = '用户总目标' + verbose_name_plural = '用户总目标' + + def __str__(self): + return f"{self.user.username}的总目标 - {self.content[:50]}..." + +class ConversationSummary(models.Model): + """对话总结模型""" + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='conversation_summaries') + talent_email = models.EmailField(verbose_name='达人邮箱') + conversation_id = models.CharField(max_length=100, verbose_name='对话ID') + summary = models.TextField(verbose_name='对话总结') + is_active = models.BooleanField(default=True, verbose_name='是否激活') + created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + updated_at = models.DateTimeField(auto_now=True, verbose_name='更新时间') + + class Meta: + db_table = 'conversation_summaries' + verbose_name = '对话总结' + verbose_name_plural = '对话总结' + + def __str__(self): + return f"{self.user.username}与{self.talent_email}的对话总结" diff --git a/user_management/urls.py b/user_management/urls.py index 68eb23c..07e1c8f 100644 --- a/user_management/urls.py +++ b/user_management/urls.py @@ -15,7 +15,6 @@ from .views import ( LogoutView, ChatHistoryViewSet, user_profile, - user_register, setup_gmail_integration, send_gmail_message, gmail_webhook, @@ -25,7 +24,10 @@ from .views import ( refresh_gmail_watch, check_gmail_auth, import_gmail_from_sender, - sync_talent_emails + sync_talent_emails, + manage_user_goal, + generate_conversation_summary, + get_recommended_reply ) # 创建路由器 @@ -51,6 +53,7 @@ urlpatterns = [ # 用户管理相关 path('users/', user_list, name='user-list'), + path('users/profile/', user_profile, name='user-profile'), path('users//', user_detail, name='user-detail'), path('users//update/', user_update, name='user-update'), path('users//delete/', user_delete, name='user-delete'), @@ -66,4 +69,9 @@ urlpatterns = [ path('gmail/check-auth/', check_gmail_auth, name='check_gmail_auth'), path('gmail/import-from-sender/', import_gmail_from_sender, name='import_gmail_from_sender'), path('gmail/sync-talent/', sync_talent_emails, name='sync_talent_emails'), + + # 新增功能API + path('user-goal/', manage_user_goal, name='manage_user_goal'), + path('conversation-summary/', generate_conversation_summary, name='generate_conversation_summary'), + path('recommended-reply/', get_recommended_reply, name='get_recommended_reply'), ] diff --git a/user_management/views.py b/user_management/views.py index 8397821..2d1c65c 100644 --- a/user_management/views.py +++ b/user_management/views.py @@ -837,7 +837,7 @@ class ChatHistoryViewSet(KnowledgeBasePermissionMixin, viewsets.ModelViewSet): "problem_optimization": False }, headers={"Content-Type": "application/json"}, - timeout=30 + ) if chat_response.status_code != 200: @@ -866,7 +866,7 @@ class ChatHistoryViewSet(KnowledgeBasePermissionMixin, viewsets.ModelViewSet): json={"message": question, "re_chat": False, "stream": True}, headers={"Content-Type": "application/json"}, stream=True, # 启用流式传输 - timeout=60 + ) if message_request.status_code != 200: @@ -1044,7 +1044,7 @@ class ChatHistoryViewSet(KnowledgeBasePermissionMixin, viewsets.ModelViewSet): url=f"{settings.API_BASE_URL}/api/application/chat/open", json=chat_request_data, headers={"Content-Type": "application/json"}, - timeout=30 + ) logger.info(f"API响应状态码: {chat_response.status_code}") @@ -1075,7 +1075,7 @@ class ChatHistoryViewSet(KnowledgeBasePermissionMixin, viewsets.ModelViewSet): url=f"{settings.API_BASE_URL}/api/application/chat_message/{chat_id}", json=message_request_data, headers={"Content-Type": "application/json"}, - timeout=60 + ) if message_response.status_code != 200: @@ -1332,7 +1332,7 @@ class ChatHistoryViewSet(KnowledgeBasePermissionMixin, viewsets.ModelViewSet): export_response = requests.get( url=export_url, - timeout=60, + stream=True # 使用流式传输处理大文件 ) @@ -1390,7 +1390,7 @@ class ChatHistoryViewSet(KnowledgeBasePermissionMixin, viewsets.ModelViewSet): response = requests.get( url=api_url, params=params, - timeout=30 + ) if response.status_code != 200: @@ -1577,7 +1577,7 @@ class ChatHistoryViewSet(KnowledgeBasePermissionMixin, viewsets.ModelViewSet): response = requests.get( url=url, params=params, - timeout=30 + ) if response.status_code != 200: @@ -2318,7 +2318,7 @@ class KnowledgeBaseViewSet(KnowledgeBasePermissionMixin, viewsets.ModelViewSet): f'{settings.API_BASE_URL}/api/dataset/{instance.external_id}', json=api_data, headers={'Content-Type': 'application/json'}, - timeout=30 + ) if response.status_code != 200: @@ -2436,7 +2436,7 @@ class KnowledgeBaseViewSet(KnowledgeBasePermissionMixin, viewsets.ModelViewSet): response = requests.delete( f'{settings.API_BASE_URL}/api/dataset/{external_id}', headers={'Content-Type': 'application/json'}, - timeout=30 + ) logger.info(f"删除外部知识库响应: status_code={response.status_code}, response={response.text}") @@ -2936,7 +2936,7 @@ class KnowledgeBaseViewSet(KnowledgeBasePermissionMixin, viewsets.ModelViewSet): response = requests.post( url, files=files_data, - timeout=60 + ) # 记录请求头和响应信息,方便排查问题 @@ -3068,7 +3068,7 @@ class KnowledgeBaseViewSet(KnowledgeBasePermissionMixin, viewsets.ModelViewSet): try: # 简单的验证请求 verify_url = f'{settings.API_BASE_URL}/api/dataset/{instance.external_id}' - verify_response = requests.get(verify_url, timeout=10) + verify_response = requests.get(verify_url) if verify_response.status_code != 200: logger.error(f"外部知识库不存在或无法访问: {instance.external_id}, 状态码: {verify_response.status_code}") return Response({ @@ -3307,7 +3307,7 @@ class KnowledgeBaseViewSet(KnowledgeBasePermissionMixin, viewsets.ModelViewSet): f'{settings.API_BASE_URL}/api/dataset', json=api_data, headers={'Content-Type': 'application/json'}, - timeout=30 + ) if response.status_code != 200: @@ -3339,7 +3339,7 @@ class KnowledgeBaseViewSet(KnowledgeBasePermissionMixin, viewsets.ModelViewSet): response = requests.delete( f'{settings.API_BASE_URL}/api/dataset/{external_id}', headers={'Content-Type': 'application/json'}, - timeout=30 + ) logger.info(f"删除外部知识库响应: status_code={response.status_code}, response={response.text}") @@ -3407,7 +3407,7 @@ class KnowledgeBaseViewSet(KnowledgeBasePermissionMixin, viewsets.ModelViewSet): response = requests.get( url, headers={'Content-Type': 'application/json'}, - timeout=30 + ) if response.status_code != 200: @@ -3534,7 +3534,7 @@ class KnowledgeBaseViewSet(KnowledgeBasePermissionMixin, viewsets.ModelViewSet): # 调用正确的外部API获取文档段落内容 try: url = f'{settings.API_BASE_URL}/api/dataset/{knowledge_base.external_id}/document/{document.external_id}/paragraph' - response = requests.get(url, timeout=30) + response = requests.get(url) if response.status_code != 200: logger.error(f"获取文档段落内容失败: {response.status_code}, {response.text}") @@ -4549,6 +4549,9 @@ class LoginView(APIView): # 获取或创建token token, _ = Token.objects.get_or_create(user=user) + # 登录用户(可选) + login(request, user) + return Response({ "code": 200, "message": "登录成功", @@ -4556,9 +4559,9 @@ class LoginView(APIView): "id": str(user.id), "username": user.username, "email": user.email, + "name": user.name, "role": user.role, "department": user.department, - "name": user.name, "group": user.group, "token": token.key } @@ -4691,7 +4694,7 @@ class RegisterView(APIView): "code": 200, "message": "注册成功", "data": { - "id": user.id, + "id": str(user.id), "username": user.username, "email": user.email, "role": user.role, @@ -4740,28 +4743,111 @@ class LogoutView(APIView): @api_view(['GET', 'PUT']) @permission_classes([IsAuthenticated]) def user_profile(request): - """获取或更新用户信息""" - if request.method == 'GET': - data = { - 'id': request.user.id, - 'username': request.user.username, - 'email': request.user.email, - 'role': request.user.role, - 'department': request.user.department, - 'phone': request.user.phone, - 'date_joined': request.user.date_joined - } - return Response(data) + """ + 获取或更新当前登录用户信息 - elif request.method == 'PUT': - user = request.user - # 只允许更新特定字段 - allowed_fields = ['email', 'phone', 'department'] - for field in allowed_fields: - if field in request.data: - setattr(user, field, request.data[field]) - user.save() - return Response({'message': '用户信息更新成功'}) + 此接口与user_update的区别: + 1. 任何已认证用户可访问 + 2. 仅能更新当前登录用户自己的信息 + 3. 不能修改角色等重要字段 + 4. 不需要指定用户ID,自动使用当前用户 + """ + try: + if request.method == 'GET': + # 检查用户是否已认证 + user = request.user + if not user.is_authenticated: + return Response({ + 'code': 401, + 'message': '用户未认证', + 'data': None + }, status=status.HTTP_401_UNAUTHORIZED) + + data = { + 'id': str(user.id), + 'username': user.username, + 'email': user.email, + 'name': user.name, + 'role': user.role, + 'department': user.department, + 'group': user.group, + 'date_joined': user.date_joined.strftime('%Y-%m-%d %H:%M:%S') + } + return Response({ + 'code': 200, + 'message': '获取用户信息成功', + 'data': data + }) + + elif request.method == 'PUT': + # 检查请求数据格式 + try: + if not request.data: + return Response({ + 'code': 400, + 'message': '请求数据为空或格式错误', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + except Exception as data_error: + return Response({ + 'code': 400, + 'message': f'请求数据格式错误: {str(data_error)}。请确保提交的是有效的JSON格式数据,属性名必须使用双引号。', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + user = request.user + # 只允许更新特定字段 + allowed_fields = ['email', 'name', 'phone', 'department', 'group'] + updated_fields = [] + + for field in allowed_fields: + if field in request.data: + setattr(user, field, request.data[field]) + updated_fields.append(field) + + if updated_fields: + try: + user.save() + return Response({ + 'code': 200, + 'message': f'用户信息更新成功,已更新字段: {", ".join(updated_fields)}', + 'data': { + 'id': str(user.id), + 'username': user.username, + 'email': user.email, + 'name': user.name, + 'role': user.role, + 'department': user.department, + 'group': user.group, + } + }) + except Exception as save_error: + logger.error(f"保存用户数据失败: {str(save_error)}") + return Response({ + 'code': 500, + 'message': f'更新用户信息失败: {str(save_error)}', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + else: + return Response({ + 'code': 400, + 'message': '没有提供任何可更新的字段', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + else: + return Response({ + 'code': 405, + 'message': f'不支持的请求方法: {request.method}', + 'data': None + }, status=status.HTTP_405_METHOD_NOT_ALLOWED) + except Exception as e: + logger.error(f"处理用户信息请求失败: {str(e)}") + logger.error(traceback.format_exc()) + return Response({ + 'code': 500, + 'message': f'处理请求失败: {str(e)}', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) @csrf_exempt @api_view(['POST']) @@ -4823,7 +4909,14 @@ def change_password(request): @api_view(['POST']) @permission_classes([AllowAny]) def user_register(request): - """用户注册""" + """ + [已弃用] 用户注册 - 请使用 RegisterView 类代替 + + 此函数仅保留用于兼容性目的,新代码应该使用 /api/auth/register/ 接口 + """ + # 打印弃用警告 + logger.warning("使用已弃用的user_register函数,请改用RegisterView类") + try: data = request.data @@ -4832,38 +4925,50 @@ def user_register(request): for field in required_fields: if not data.get(field): return Response({ - 'error': f'缺少必填字段: {field}' + 'code': 400, + 'message': f'缺少必填字段: {field}', + 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 验证角色 valid_roles = ['admin', 'leader', 'member'] if data['role'] not in valid_roles: return Response({ - 'error': f'无效的角色,必须是: {", ".join(valid_roles)}' + 'code': 400, + 'message': f'无效的角色,必须是: {", ".join(valid_roles)}', + 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 如果是组员,必须指定小组 if data['role'] == 'member' and not data.get('group'): return Response({ - 'error': '组员必须指定所属小组' + 'code': 400, + 'message': '组员必须指定所属小组', + 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 检查用户名是否已存在 if User.objects.filter(username=data['username']).exists(): return Response({ - 'error': '用户名已存在' + 'code': 400, + 'message': '用户名已存在', + 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 检查邮箱是否已存在 if User.objects.filter(email=data['email']).exists(): return Response({ - 'error': '邮箱已被注册' + 'code': 400, + 'message': '邮箱已被注册', + 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 验证密码强度 if len(data['password']) < 8: return Response({ - 'error': '密码长度必须至少为8位' + 'code': 400, + 'message': '密码长度必须至少为8位', + 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 验证邮箱格式 @@ -4871,7 +4976,9 @@ def user_register(request): validate_email(data['email']) except ValidationError: return Response({ - 'error': '邮箱格式不正确' + 'code': 400, + 'message': '邮箱格式不正确', + 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 创建用户 @@ -4891,9 +4998,10 @@ def user_register(request): token, _ = Token.objects.get_or_create(user=user) return Response({ + 'code': 200, 'message': '注册成功', 'data': { - 'id': user.id, + 'id': str(user.id), 'username': user.username, 'email': user.email, 'role': user.role, @@ -4906,82 +5014,41 @@ def user_register(request): }, status=status.HTTP_201_CREATED) except Exception as e: - print(f"注册失败: {str(e)}") - print(f"错误类型: {type(e)}") - print(f"错误堆栈: {traceback.format_exc()}") + logger.error(f"注册失败: {str(e)}") + logger.error(f"错误类型: {type(e)}") + logger.error(f"错误堆栈: {traceback.format_exc()}") return Response({ - 'error': f'注册失败: {str(e)}', + 'code': 500, + 'message': f'注册失败: {str(e)}', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) -@csrf_exempt -@api_view(['POST']) -@permission_classes([IsAuthenticated]) -def verify_token(request): - """验证令牌有效性""" - try: - return Response({ - "code": 200, - "message": "令牌有效", - "data": { - "is_valid": True, - "user": { - "id": request.user.id, - "username": request.user.username, - "email": request.user.email, - "role": request.user.role, - "department": request.user.department, - "name": request.user.name, - "group": request.user.group - } - } - }) - except Exception as e: - return Response({ - "code": 500, - "message": f"验证失败: {str(e)}", - "data": None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - -@api_view(['GET']) -@permission_classes([IsAuthenticated]) -def user_list(request): - """获取用户列表""" - user = request.user - if user.role == 'admin': - users = User.objects.all() - elif user.role == 'leader': - users = User.objects.filter(department=user.department) - else: - users = User.objects.filter(id=user.id) - - data = [{ - 'id': u.id, - 'username': u.username, - 'email': u.email, - 'role': u.role, - 'department': u.department, - 'is_active': u.is_active, - 'date_joined': u.date_joined - } for u in users] - - return Response(data) - @api_view(['GET']) @permission_classes([IsAuthenticated]) def user_detail(request, pk): """获取用户详情""" try: - # 尝试转换为 UUID - if not isinstance(pk, uuid.UUID): - try: - pk = uuid.UUID(pk) - except ValueError: - return Response({ - "code": 400, - "message": "无效的用户ID格式", - "data": None - }, status=status.HTTP_400_BAD_REQUEST) + # 尝试转换为 UUID,处理多种可能的格式 + try: + if not isinstance(pk, uuid.UUID): + # 移除所有空格,以防万一 + pk = pk.strip() + + # 处理带连字符和不带连字符的格式 + if '-' not in pk and len(pk) == 32: + # 转换没有连字符的UUID格式 + pk_with_hyphens = f"{pk[0:8]}-{pk[8:12]}-{pk[12:16]}-{pk[16:20]}-{pk[20:]}" + pk = uuid.UUID(pk_with_hyphens) + else: + # 尝试直接转换 + pk = uuid.UUID(pk) + except ValueError: + # 提供更详细的错误信息 + return Response({ + "code": 400, + "message": f"无效的用户ID格式: {pk}。用户ID应为有效的UUID格式。", + "data": None + }, status=status.HTTP_400_BAD_REQUEST) user = get_object_or_404(User, pk=pk) @@ -4999,6 +5066,8 @@ def user_detail(request, pk): } }) except Exception as e: + logger.error(f"获取用户信息失败: {str(e)}") + logger.error(traceback.format_exc()) return Response({ "code": 500, "message": f"获取用户信息失败: {str(e)}", @@ -5008,29 +5077,272 @@ def user_detail(request, pk): @api_view(['PUT']) @permission_classes([IsAdminUser]) def user_update(request, pk): - """更新用户信息""" + """ + 管理员更新用户信息 + + 此接口与user_profile的区别: + 1. 仅管理员可访问 + 2. 可以更新任何用户的信息 + 3. 可以修改角色等重要字段 + 4. 需要在URL中指定用户ID + """ try: - user = User.objects.get(pk=pk) + # 检查请求数据格式 + try: + if not request.data: + return Response({ + 'code': 400, + 'message': '请求数据为空或格式错误', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + except Exception as data_error: + return Response({ + 'code': 400, + 'message': f'请求数据格式错误: {str(data_error)}。请确保提交的是有效的JSON格式数据,属性名必须使用双引号。', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + # 尝试转换为 UUID + try: + if not isinstance(pk, uuid.UUID): + pk = pk.strip() + + # 处理带连字符和不带连字符的格式 + if '-' not in pk and len(pk) == 32: + # 转换没有连字符的UUID格式 + pk_with_hyphens = f"{pk[0:8]}-{pk[8:12]}-{pk[12:16]}-{pk[16:20]}-{pk[20:]}" + pk = uuid.UUID(pk_with_hyphens) + else: + # 尝试直接转换 + pk = uuid.UUID(pk) + except ValueError: + return Response({ + "code": 400, + "message": f"无效的用户ID格式: {pk}。用户ID应为有效的UUID格式。", + "data": None + }, status=status.HTTP_400_BAD_REQUEST) + + try: + user = User.objects.get(pk=pk) + except User.DoesNotExist: + return Response({ + 'code': 404, + 'message': '用户不存在', + 'data': None + }, status=status.HTTP_404_NOT_FOUND) + # 只允许更新特定字段 - allowed_fields = ['email', 'role', 'department', 'is_active', 'phone'] + allowed_fields = ['email', 'role', 'department', 'group', 'is_active', 'phone', 'name'] + updated_fields = [] + for field in allowed_fields: if field in request.data: setattr(user, field, request.data[field]) + updated_fields.append(field) + + if not updated_fields: + return Response({ + 'code': 400, + 'message': '没有提供任何可更新的字段', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + user.save() - return Response({'message': '用户信息更新成功'}) - except User.DoesNotExist: - return Response({'message': '用户不存在'}, status=404) + + return Response({ + 'code': 200, + 'message': '用户信息更新成功', + 'data': { + 'id': str(user.id), + 'username': user.username, + 'email': user.email, + 'name': user.name, + 'role': user.role, + 'department': user.department, + 'group': user.group, + 'is_active': user.is_active + } + }) + except Exception as e: + logger.error(f"更新用户信息失败: {str(e)}") + logger.error(traceback.format_exc()) + return Response({ + 'code': 500, + 'message': f'更新用户信息失败: {str(e)}', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) @api_view(['DELETE']) @permission_classes([IsAdminUser]) def user_delete(request, pk): """删除用户""" try: - user = User.objects.get(pk=pk) + # 尝试转换为 UUID + try: + if not isinstance(pk, uuid.UUID): + pk = pk.strip() + + # 处理带连字符和不带连字符的格式 + if '-' not in pk and len(pk) == 32: + # 转换没有连字符的UUID格式 + pk_with_hyphens = f"{pk[0:8]}-{pk[8:12]}-{pk[12:16]}-{pk[16:20]}-{pk[20:]}" + pk = uuid.UUID(pk_with_hyphens) + else: + # 尝试直接转换 + pk = uuid.UUID(pk) + except ValueError: + return Response({ + "code": 400, + "message": f"无效的用户ID格式: {pk}。用户ID应为有效的UUID格式。", + "data": None + }, status=status.HTTP_400_BAD_REQUEST) + + try: + user = User.objects.get(pk=pk) + except User.DoesNotExist: + return Response({ + 'code': 404, + 'message': '用户不存在', + 'data': None + }, status=status.HTTP_404_NOT_FOUND) + + # 检查是否试图删除管理员账户 + if user.is_superuser or user.role == 'admin': + return Response({ + 'code': 403, + 'message': '不允许删除管理员账户', + 'data': None + }, status=status.HTTP_403_FORBIDDEN) + + # 删除用户 + username = user.username user.delete() - return Response({'message': '用户删除成功'}) - except User.DoesNotExist: - return Response({'message': '用户不存在'}, status=404) + + return Response({ + 'code': 200, + 'message': f'用户 {username} 删除成功', + 'data': None + }) + except Exception as e: + logger.error(f"删除用户失败: {str(e)}") + logger.error(traceback.format_exc()) + return Response({ + 'code': 500, + 'message': f'删除用户失败: {str(e)}', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + +@csrf_exempt +@api_view(['POST']) +@permission_classes([IsAuthenticated]) +def verify_token(request): + """验证令牌有效性""" + try: + return Response({ + "code": 200, + "message": "令牌有效", + "data": { + "is_valid": True, + "user": { + "id": str(request.user.id), + "username": request.user.username, + "email": request.user.email, + "name": request.user.name, + "role": request.user.role, + "department": request.user.department, + "group": request.user.group + } + } + }) + except Exception as e: + logger.error(f"验证令牌失败: {str(e)}") + logger.error(traceback.format_exc()) + return Response({ + "code": 500, + "message": f"验证失败: {str(e)}", + "data": None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + +@api_view(['GET']) +@permission_classes([IsAuthenticated]) +def user_list(request): + """获取用户列表""" + try: + # 获取查询参数 + page = int(request.query_params.get('page', 1)) + page_size = int(request.query_params.get('page_size', 20)) + keyword = request.query_params.get('keyword', '') + + # 根据用户角色获取不同范围的用户列表 + user = request.user + base_query = User.objects.all() + + if user.role == 'admin': + users_query = base_query + elif user.role == 'leader': + users_query = base_query.filter(department=user.department) + else: + users_query = base_query.filter(id=user.id) + + # 添加关键字搜索 + if keyword: + users_query = users_query.filter( + Q(username__icontains=keyword) | + Q(email__icontains=keyword) | + Q(name__icontains=keyword) | + Q(department__icontains=keyword) + ) + + # 计算总数 + total = users_query.count() + + # 分页 + start = (page - 1) * page_size + end = start + page_size + users = users_query[start:end] + + # 构造数据 + user_data = [] + for u in users: + user_data.append({ + 'id': str(u.id), + 'username': u.username, + 'email': u.email, + 'name': u.name, + 'role': u.role, + 'department': u.department, + 'group': u.group, + 'is_active': u.is_active, + 'date_joined': u.date_joined.strftime('%Y-%m-%d %H:%M:%S') + }) + + return Response({ + 'code': 200, + 'message': '获取用户列表成功', + 'data': { + 'total': total, + 'page': page, + 'page_size': page_size, + 'users': user_data + } + }) + + except ValueError as e: + # 处理参数转换错误 + return Response({ + 'code': 400, + 'message': f'参数错误: {str(e)}', + 'data': None + }, status=status.HTTP_400_BAD_REQUEST) + + except Exception as e: + logger.error(f"获取用户列表失败: {str(e)}") + logger.error(traceback.format_exc()) + return Response({ + 'code': 500, + 'message': f'获取用户列表失败: {str(e)}', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) from rest_framework.decorators import api_view, permission_classes from rest_framework.permissions import IsAuthenticated @@ -5997,3 +6309,610 @@ def sync_talent_emails(request): 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) +@api_view(['POST']) +@permission_classes([IsAuthenticated]) +def manage_user_goal(request): + """ + 管理用户总目标的API视图 + + POST请求参数: + - content: 用户总目标内容(可选,不提供则返回当前总目标) + + 返回: + - 包含操作结果和总目标信息的JSON响应 + """ + user = request.user + goal_content = request.data.get('content') + + try: + from .gmail_integration import GmailIntegration + + # 创建Gmail集成实例 + gmail_integration = GmailIntegration(user) + result = gmail_integration.manage_user_goal(goal_content) + + return Response(result, status=status.HTTP_200_OK) + + except Exception as e: + return Response( + {'status': 'error', 'message': str(e)}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + +@api_view(['POST']) +@permission_classes([IsAuthenticated]) +def generate_conversation_summary(request): + """ + 生成与达人的对话总结的API视图 + + POST请求参数: + - talent_email: 达人的邮箱地址 + + 返回: + - 包含操作结果和总结信息的JSON响应 + """ + user = request.user + talent_email = request.data.get('talent_email') + + if not talent_email: + return Response( + {'status': 'error', 'message': '缺少必要参数: talent_email'}, + status=status.HTTP_400_BAD_REQUEST + ) + + try: + from .gmail_integration import GmailIntegration + + # 创建Gmail集成实例 + gmail_integration = GmailIntegration(user) + result = gmail_integration.generate_conversation_summary(talent_email) + + return Response(result, status=status.HTTP_200_OK) + + except Exception as e: + return Response( + {'status': 'error', 'message': str(e)}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + +@api_view(['POST']) +@permission_classes([IsAuthenticated]) +def get_recommended_reply(request): + """ + 获取针对达人最后一条消息的推荐回复的API视图 + + POST请求参数: + - conversation_id: 对话ID + - talent_email: 达人的邮箱地址 + + 返回: + - 包含推荐回复的JSON响应 + """ + user = request.user + conversation_id = request.data.get('conversation_id') + talent_email = request.data.get('talent_email') + + if not conversation_id or not talent_email: + return Response( + {'status': 'error', 'message': '缺少必要参数: conversation_id 或 talent_email'}, + status=status.HTTP_400_BAD_REQUEST + ) + + try: + from .gmail_integration import GmailIntegration + from .models import ChatHistory + + # 获取对话历史 + chat_history = ChatHistory.objects.filter( + conversation_id=conversation_id, + user=user, + is_deleted=False + ).order_by('created_at') + + if not chat_history: + return Response( + {'status': 'error', 'message': '找不到对话历史'}, + status=status.HTTP_404_NOT_FOUND + ) + + # 转换为DeepSeek API所需的格式 + conversation_history = [] + for message in chat_history: + role = 'user' if message.role == 'user' else 'assistant' + message_data = { + 'role': role, + 'content': message.content, + 'metadata': { + 'conversation_id': conversation_id + } + } + + # 确定消息是否来自达人 + if role == 'user': + message_data['metadata']['from_email'] = talent_email + + conversation_history.append(message_data) + + # 创建Gmail集成实例并获取推荐回复 + gmail_integration = GmailIntegration(user) + reply = gmail_integration._get_recommended_reply_from_deepseek(conversation_history) + + if not reply: + return Response( + {'status': 'error', 'message': '生成推荐回复失败'}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + return Response({ + 'status': 'success', + 'reply': reply + }, status=status.HTTP_200_OK) + + except Exception as e: + return Response( + {'status': 'error', 'message': str(e)}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + +@csrf_exempt +@api_view(['POST']) +@permission_classes([IsAuthenticated]) +def feishu_sync_api(request): + """飞书数据同步API + + 支持自定义凭据和批量处理 + """ + # 检查权限 + if not (request.user.has_perm('feishu.can_sync_feishu') or request.user.role in ['admin', 'leader']): + return Response({ + 'code': 403, + 'message': '您没有同步飞书数据的权限' + }, status=403) + + # 获取参数 + app_token = request.data.get('app_token') + table_id = request.data.get('table_id') + user_access_token = request.data.get('user_access_token') + sync_all = request.data.get('sync_all', False) + sync_to_kb = request.data.get('sync_to_kb', False) + creator_ids = request.data.get('creator_ids', []) + + # 验证参数 + if not sync_all and not creator_ids: + return Response({ + 'code': 400, + 'message': '请指定sync_all=true或提供creator_ids列表' + }, status=400) + + try: + from feishu.feishu import sync_from_feishu, sync_to_knowledge_base + from user_management.models import FeishuCreator + + # 根据参数同步数据 + if sync_all: + # 同步所有数据 + result = sync_from_feishu( + app_token=app_token, + table_id=table_id, + user_access_token=user_access_token + ) + + if 'error_message' in result: + return Response({ + 'code': 500, + 'message': f'同步失败: {result["error_message"]}', + 'data': result + }, status=500) + + # 处理同步到知识库 + if sync_to_kb and result.get('created_creators'): + kb_results = [] + for creator in result.get('created_creators', []): + if creator.email: # 只处理有邮箱的达人 + kb, created = sync_to_knowledge_base(creator_id=creator.id) + if kb: + kb_results.append({ + 'creator_id': creator.id, + 'handle': creator.handle, + 'kb_id': kb.id, + 'kb_name': kb.name, + 'created': created + }) + + result['kb_sync'] = { + 'total': len(kb_results), + 'results': kb_results + } + + return Response({ + 'code': 200, + 'message': '同步成功', + 'data': result + }) + else: + # 处理指定的creator_ids + results = [] + for creator_id in creator_ids: + try: + creator = FeishuCreator.objects.get(id=creator_id) + # 这里可以添加特定的处理逻辑 + + # 如果需要同步到知识库 + if sync_to_kb: + kb, created = sync_to_knowledge_base(creator_id=creator_id) + results.append({ + 'creator_id': creator_id, + 'handle': creator.handle if creator else None, + 'success': True, + 'kb_sync': { + 'success': kb is not None, + 'kb_id': kb.id if kb else None, + 'created': created + } + }) + else: + results.append({ + 'creator_id': creator_id, + 'handle': creator.handle, + 'success': True + }) + except FeishuCreator.DoesNotExist: + results.append({ + 'creator_id': creator_id, + 'success': False, + 'message': '达人不存在' + }) + except Exception as e: + results.append({ + 'creator_id': creator_id, + 'success': False, + 'message': str(e) + }) + + return Response({ + 'code': 200, + 'message': '处理完成', + 'data': { + 'total': len(results), + 'results': results + } + }) + + except Exception as e: + import traceback + logger.error(f"飞书同步API错误: {str(e)}") + logger.error(traceback.format_exc()) + return Response({ + 'code': 500, + 'message': f'系统错误: {str(e)}', + 'data': None + }, status=500) + +@api_view(['POST']) +@permission_classes([IsAuthenticated]) +def feishu_to_kb_api(request): + """将飞书数据同步到知识库API + + 支持批量处理 + """ + if not (request.user.has_perm('feishu.can_sync_feishu') or request.user.role in ['admin', 'leader']): + return Response({ + 'code': 403, + 'message': '您没有同步飞书数据的权限' + }, status=403) + + # 获取参数 + creator_id = request.data.get('creator_id') + handle = request.data.get('handle') + email = request.data.get('email') + batch_mode = request.data.get('batch_mode', False) + has_email = request.data.get('has_email', False) + no_kb = request.data.get('no_kb', False) + + try: + from feishu.feishu import sync_to_knowledge_base + from user_management.models import FeishuCreator + from user_management.models import KnowledgeBase + + # 批量模式 + if batch_mode: + query = FeishuCreator.objects.all() + + # 筛选条件: 只处理有邮箱的达人 + if has_email: + query = query.exclude(email__isnull=True).exclude(email='') + + # 筛选条件: 只处理没有知识库的达人 + if no_kb: + # 这里需要根据实际情况实现筛选逻辑 + # 可能需要一个辅助函数来检查哪些达人没有知识库 + # 这里是一个简化示例 + creators_with_kb = [] + from user_management.models import GmailTalentMapping + kb_mappings = GmailTalentMapping.objects.filter(is_active=True) + for mapping in kb_mappings: + email = mapping.talent_email + if email: + creators_with_kb.append(email) + + if creators_with_kb: + query = query.exclude(email__in=creators_with_kb) + + creators = query.all() + + # 处理结果 + results = [] + success_count = 0 + error_count = 0 + + for creator in creators: + try: + kb, created = sync_to_knowledge_base(creator_id=creator.id) + if kb: + results.append({ + 'creator_id': creator.id, + 'handle': creator.handle, + 'email': creator.email, + 'success': True, + 'kb_id': kb.id, + 'kb_name': kb.name, + 'created': created + }) + success_count += 1 + else: + results.append({ + 'creator_id': creator.id, + 'handle': creator.handle, + 'email': creator.email, + 'success': False, + 'message': '知识库创建失败' + }) + error_count += 1 + except Exception as e: + results.append({ + 'creator_id': creator.id, + 'handle': creator.handle if hasattr(creator, 'handle') else None, + 'email': creator.email if hasattr(creator, 'email') else None, + 'success': False, + 'message': str(e) + }) + error_count += 1 + + return Response({ + 'code': 200, + 'message': '批量处理完成', + 'data': { + 'total': len(results), + 'success': success_count, + 'error': error_count, + 'results': results + } + }) + + # 单个处理模式 + elif creator_id or handle or email: + kb, created = sync_to_knowledge_base( + creator_id=creator_id, + handle=handle, + email=email + ) + + if kb: + return Response({ + 'code': 200, + 'message': '同步成功', + 'data': { + 'kb_id': kb.id, + 'kb_name': kb.name, + 'created': created + } + }) + else: + return Response({ + 'code': 400, + 'message': '同步失败,未能创建或找到知识库', + 'data': None + }, status=400) + else: + return Response({ + 'code': 400, + 'message': '请提供creator_id、handle或email参数', + 'data': None + }, status=400) + + except Exception as e: + import traceback + logger.error(f"飞书数据同步到知识库API错误: {str(e)}") + logger.error(traceback.format_exc()) + return Response({ + 'code': 500, + 'message': f'系统错误: {str(e)}', + 'data': None + }, status=500) + +@api_view(['GET', 'POST']) +@permission_classes([IsAuthenticated]) +def check_creator_kb_api(request): + """检查达人是否有知识库API + + GET方法: 检查单个达人 + POST方法: 批量检查多个达人 + """ + if not request.user.has_perm('feishu.can_view_feishu'): + return Response({ + 'code': 403, + 'message': '您没有查看飞书数据的权限' + }, status=403) + + try: + from user_management.models import FeishuCreator + from user_management.models import GmailTalentMapping + from user_management.models import KnowledgeBase + + # GET方法: 检查单个达人 + if request.method == 'GET': + creator_id = request.query_params.get('creator_id') + handle = request.query_params.get('handle') + email = request.query_params.get('email') + + if not any([creator_id, handle, email]): + return Response({ + 'code': 400, + 'message': '请提供creator_id、handle或email参数', + 'data': None + }, status=400) + + # 查找达人 + query = FeishuCreator.objects.all() + if creator_id: + query = query.filter(id=creator_id) + elif handle: + query = query.filter(handle=handle) + elif email: + query = query.filter(email=email) + + creator = query.first() + + if not creator: + return Response({ + 'code': 404, + 'message': '未找到达人', + 'data': None + }, status=404) + + # 检查知识库 + kb_info = None + + # 通过Email检查Gmail映射 + if creator.email: + gmail_mapping = GmailTalentMapping.objects.filter( + talent_email=creator.email, + is_active=True + ).first() + + if gmail_mapping and gmail_mapping.knowledge_base: + kb = gmail_mapping.knowledge_base + kb_info = { + 'kb_id': kb.id, + 'kb_name': kb.name, + 'mapping_type': 'gmail', + 'mapping_id': gmail_mapping.id + } + + # 也可以检查其他映射方式,如直接通过名称匹配 + if not kb_info and creator.handle: + kb_name = f"达人-{creator.handle}" + kb = KnowledgeBase.objects.filter(name=kb_name, is_active=True).first() + + if kb: + kb_info = { + 'kb_id': kb.id, + 'kb_name': kb.name, + 'mapping_type': 'direct', + 'mapping_id': None + } + + return Response({ + 'code': 200, + 'message': '查询成功', + 'data': { + 'creator_id': creator.id, + 'handle': creator.handle, + 'email': creator.email, + 'has_kb': kb_info is not None, + 'kb_info': kb_info + } + }) + + # POST方法: 批量检查 + else: + creator_ids = request.data.get('creator_ids', []) + + if not creator_ids: + return Response({ + 'code': 400, + 'message': '请提供creator_ids参数', + 'data': None + }, status=400) + + results = [] + + # 预先加载所有Gmail映射 + gmail_mappings = {} + for mapping in GmailTalentMapping.objects.filter(is_active=True): + if mapping.talent_email: + gmail_mappings[mapping.talent_email] = mapping + + # 处理每个达人 + for creator_id in creator_ids: + try: + creator = FeishuCreator.objects.get(id=creator_id) + + # 检查知识库 + kb_info = None + + # 通过Email检查Gmail映射 + if creator.email and creator.email in gmail_mappings: + mapping = gmail_mappings[creator.email] + if mapping.knowledge_base: + kb = mapping.knowledge_base + kb_info = { + 'kb_id': kb.id, + 'kb_name': kb.name, + 'mapping_type': 'gmail', + 'mapping_id': mapping.id + } + + # 也可以检查其他映射方式 + if not kb_info and creator.handle: + kb_name = f"达人-{creator.handle}" + kb = KnowledgeBase.objects.filter(name=kb_name, is_active=True).first() + + if kb: + kb_info = { + 'kb_id': kb.id, + 'kb_name': kb.name, + 'mapping_type': 'direct', + 'mapping_id': None + } + + results.append({ + 'creator_id': creator.id, + 'handle': creator.handle, + 'email': creator.email, + 'has_kb': kb_info is not None, + 'kb_info': kb_info + }) + + except FeishuCreator.DoesNotExist: + results.append({ + 'creator_id': creator_id, + 'success': False, + 'message': '达人不存在' + }) + except Exception as e: + results.append({ + 'creator_id': creator_id, + 'success': False, + 'message': str(e) + }) + + return Response({ + 'code': 200, + 'message': '批量查询成功', + 'data': { + 'total': len(results), + 'results': results + } + }) + + except Exception as e: + import traceback + logger.error(f"检查达人知识库API错误: {str(e)}") + logger.error(traceback.format_exc()) + return Response({ + 'code': 500, + 'message': f'系统错误: {str(e)}', + 'data': None + }, status=500) +