Compare commits
10 Commits
af5aed7c0f
...
45d49d4b4a
Author | SHA1 | Date | |
---|---|---|---|
![]() |
45d49d4b4a | ||
![]() |
083971a94a | ||
![]() |
b64af631e3 | ||
![]() |
5eb4ccd7fd | ||
![]() |
a90d31c420 | ||
![]() |
979967ed7d | ||
![]() |
9153594375 | ||
![]() |
c0bba14ee8 | ||
![]() |
00deeb711b | ||
![]() |
dfef072a8c |
BIN
data_backup.json
Normal file
BIN
data_backup.json
Normal file
Binary file not shown.
59
deploy.ps1
59
deploy.ps1
@ -1,59 +0,0 @@
|
|||||||
# 确保在项目根目录下执行此脚本
|
|
||||||
|
|
||||||
# 7-Zip路径 - 请根据实际安装位置修改
|
|
||||||
$7zipPath = "D:\7zip\7-Zip\7z.exe" # 原始路径
|
|
||||||
# $7zipPath = "C:\Program Files\7-Zip\7z.exe" # 修改后的路径
|
|
||||||
|
|
||||||
# 创建临时部署目录
|
|
||||||
New-Item -Path "deploy_temp" -ItemType Directory -Force
|
|
||||||
|
|
||||||
# 复制整个项目目录到临时目录
|
|
||||||
Copy-Item -Path "role_based_system" -Destination "deploy_temp\" -Recurse -Force
|
|
||||||
Copy-Item -Path "user_management" -Destination "deploy_temp\" -Recurse -Force
|
|
||||||
Copy-Item -Path "manage.py" -Destination "deploy_temp\" -Force
|
|
||||||
Copy-Item -Path "requirements.txt" -Destination "deploy_temp\" -Force
|
|
||||||
Copy-Item -Path "*.md" -Destination "deploy_temp\" -Force -ErrorAction SilentlyContinue
|
|
||||||
|
|
||||||
# 移除不需要的文件和目录
|
|
||||||
Get-ChildItem -Path "deploy_temp" -Recurse -Filter "__pycache__" -Directory | Remove-Item -Recurse -Force
|
|
||||||
Get-ChildItem -Path "deploy_temp" -Recurse -Filter "*.pyc" | Remove-Item -Force
|
|
||||||
|
|
||||||
# 特别排除不需要的目录
|
|
||||||
$excludeDirs = @(
|
|
||||||
".git", ".idea", ".vscode",
|
|
||||||
"venv", ".venv", "env", "__pycache__", "migrations"
|
|
||||||
)
|
|
||||||
|
|
||||||
foreach ($dir in $excludeDirs) {
|
|
||||||
Get-ChildItem -Path "deploy_temp" -Recurse -Directory -Filter $dir |
|
|
||||||
Where-Object { $_.FullName -notmatch "\\migrations\\__pycache__" } |
|
|
||||||
Remove-Item -Recurse -Force -ErrorAction SilentlyContinue
|
|
||||||
}
|
|
||||||
|
|
||||||
# 保留migrations目录但删除其中的pyc文件
|
|
||||||
if (Test-Path "deploy_temp\user_management\migrations") {
|
|
||||||
Get-ChildItem -Path "deploy_temp\user_management\migrations" -Filter "*.pyc" | Remove-Item -Force
|
|
||||||
Get-ChildItem -Path "deploy_temp\user_management\migrations" -Directory -Filter "__pycache__" | Remove-Item -Recurse -Force
|
|
||||||
}
|
|
||||||
|
|
||||||
# 排除不需要的文件
|
|
||||||
$excludeFiles = @(
|
|
||||||
"*.pyc", "*.pyo", "*.pyd", "*.so", "*.dll",
|
|
||||||
"*.db", "*.sqlite3", "*.log", "*.zip", "*.tar.gz",
|
|
||||||
"local_settings.py", "*.bak"
|
|
||||||
)
|
|
||||||
|
|
||||||
foreach ($pattern in $excludeFiles) {
|
|
||||||
Get-ChildItem -Path "deploy_temp" -Recurse -Filter $pattern | Remove-Item -Force -ErrorAction SilentlyContinue
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# 使用7-Zip打包
|
|
||||||
& $7zipPath a -ttar knowledge_system.tar ".\deploy_temp\*"
|
|
||||||
& $7zipPath a -tgzip knowledge_system.tar.gz knowledge_system.tar
|
|
||||||
|
|
||||||
# 清理临时文件
|
|
||||||
Remove-Item -Path "knowledge_system.tar" -Force
|
|
||||||
Remove-Item -Path "deploy_temp" -Recurse -Force
|
|
||||||
|
|
||||||
Write-Host "部署包已创建: knowledge_system.tar.gz" -ForegroundColor Green
|
|
0
feishu/__init__.py
Normal file
0
feishu/__init__.py
Normal file
290
feishu/feishu.py
Normal file
290
feishu/feishu.py
Normal file
@ -0,0 +1,290 @@
|
|||||||
|
import json
|
||||||
|
import django
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import pandas as pd
|
||||||
|
from django.db import transaction
|
||||||
|
|
||||||
|
# 设置 Django 环境
|
||||||
|
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'role_based_system.settings')
|
||||||
|
django.setup()
|
||||||
|
|
||||||
|
import lark_oapi as lark
|
||||||
|
from lark_oapi.api.bitable.v1 import *
|
||||||
|
from user_management.models import FeishuCreator
|
||||||
|
|
||||||
|
|
||||||
|
# SDK 使用说明: https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/server-side-sdk/python--sdk/preparations-before-development
|
||||||
|
# 以下示例代码默认根据文档示例值填充,如果存在代码问题,请在 API 调试台填上相关必要参数后再复制代码使用
|
||||||
|
def extract_field_value(field_value):
|
||||||
|
"""提取字段值"""
|
||||||
|
if isinstance(field_value, list):
|
||||||
|
if field_value and isinstance(field_value[0], dict):
|
||||||
|
return field_value[0].get('text', '')
|
||||||
|
elif isinstance(field_value, dict):
|
||||||
|
if 'text' in field_value:
|
||||||
|
return field_value['text']
|
||||||
|
elif 'link' in field_value:
|
||||||
|
return field_value['link']
|
||||||
|
elif 'link_record_ids' in field_value:
|
||||||
|
return ''
|
||||||
|
return field_value
|
||||||
|
|
||||||
|
def save_to_database(record):
|
||||||
|
"""从飞书多维表格保存记录到数据库"""
|
||||||
|
fields = record.fields
|
||||||
|
record_id = record.record_id
|
||||||
|
|
||||||
|
creator_data = {
|
||||||
|
'record_id': record_id,
|
||||||
|
'contact_person': extract_field_value(fields.get('对接人', '')),
|
||||||
|
'handle': extract_field_value(fields.get('Handle', '')),
|
||||||
|
'tiktok_url': extract_field_value(fields.get('链接', '')),
|
||||||
|
'fans_count': extract_field_value(fields.get('粉丝数', '')),
|
||||||
|
'gmv': fields.get('GMV', ''),
|
||||||
|
'email': extract_field_value(fields.get('邮箱', '')),
|
||||||
|
'phone': extract_field_value(fields.get('手机号|WhatsApp', '')),
|
||||||
|
'account_type': extract_field_value(fields.get('账号属性', [])),
|
||||||
|
'price_quote': fields.get('报价', ''),
|
||||||
|
'response_speed': fields.get('回复速度', ''),
|
||||||
|
'cooperation_intention': fields.get('合作意向', ''),
|
||||||
|
'payment_method': fields.get('支付方式', ''),
|
||||||
|
'payment_account': fields.get('收款账号', ''),
|
||||||
|
'address': fields.get('收件地址', ''),
|
||||||
|
'has_ooin': fields.get('签约OOIN?', ''),
|
||||||
|
'source': fields.get('渠道来源', ''),
|
||||||
|
'contact_status': fields.get('建联进度', ''),
|
||||||
|
'cooperation_brands': fields.get('合作品牌', []),
|
||||||
|
'system_categories': extract_field_value(fields.get('系统展示的带货品类', [])),
|
||||||
|
'actual_categories': extract_field_value(fields.get('实际高播放量带货品类', [])),
|
||||||
|
'human_categories': fields.get('达人标想要货品类', ''),
|
||||||
|
'creator_base': '',
|
||||||
|
'notes': extract_field_value(fields.get('父记录', '')),
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
creator, created = FeishuCreator.objects.update_or_create(
|
||||||
|
record_id=record_id,
|
||||||
|
defaults=creator_data
|
||||||
|
)
|
||||||
|
return creator, created
|
||||||
|
except Exception as e:
|
||||||
|
print(f"保存记录时出错: {str(e)}")
|
||||||
|
print(f"记录数据: {creator_data}")
|
||||||
|
return None, False
|
||||||
|
|
||||||
|
def fetch_all_records(client, app_token, table_id, user_access_token):
|
||||||
|
"""获取所有记录"""
|
||||||
|
total_records = []
|
||||||
|
page_token = None
|
||||||
|
page_size = 20
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
# 构造请求对象
|
||||||
|
builder = SearchAppTableRecordRequest.builder() \
|
||||||
|
.app_token(app_token) \
|
||||||
|
.table_id(table_id) \
|
||||||
|
.page_size(page_size)
|
||||||
|
|
||||||
|
# 如果有page_token,添加到请求中
|
||||||
|
if page_token:
|
||||||
|
builder = builder.page_token(page_token)
|
||||||
|
|
||||||
|
# 构建完整请求
|
||||||
|
request = builder.request_body(SearchAppTableRecordRequestBody.builder().build()).build()
|
||||||
|
|
||||||
|
print(f"发送请求,page_token: {page_token}")
|
||||||
|
|
||||||
|
# 发起请求
|
||||||
|
option = lark.RequestOption.builder().user_access_token(user_access_token).build()
|
||||||
|
response = client.bitable.v1.app_table_record.search(request, option)
|
||||||
|
|
||||||
|
if not response.success():
|
||||||
|
print(f"请求失败: {response.code}, {response.msg}")
|
||||||
|
break
|
||||||
|
|
||||||
|
# 获取当前页记录
|
||||||
|
current_records = response.data.items
|
||||||
|
if not current_records:
|
||||||
|
print("没有更多记录")
|
||||||
|
break
|
||||||
|
|
||||||
|
total_records.extend(current_records)
|
||||||
|
|
||||||
|
# 解析响应数据获取分页信息
|
||||||
|
response_data = json.loads(response.raw.content)
|
||||||
|
total = response_data["data"]["total"]
|
||||||
|
print(f"获取到 {len(current_records)} 条记录,当前总计: {len(total_records)}/{total} 条")
|
||||||
|
|
||||||
|
# 获取下一页token
|
||||||
|
page_token = response_data["data"].get("page_token")
|
||||||
|
if not page_token or not response_data["data"].get("has_more", False):
|
||||||
|
print("已获取所有数据")
|
||||||
|
break
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"错误: {str(e)}")
|
||||||
|
import traceback
|
||||||
|
print(traceback.format_exc())
|
||||||
|
break
|
||||||
|
|
||||||
|
print(f"最终获取到 {len(total_records)} 条记录")
|
||||||
|
return total_records
|
||||||
|
|
||||||
|
def update_from_excel(excel_file_path):
|
||||||
|
"""从Excel文件更新数据库记录"""
|
||||||
|
try:
|
||||||
|
print(f"开始读取Excel文件: {excel_file_path}")
|
||||||
|
df = pd.read_excel(excel_file_path)
|
||||||
|
|
||||||
|
if 'Handle' not in df.columns:
|
||||||
|
print("错误: Excel文件中缺少'Handle'列")
|
||||||
|
return
|
||||||
|
|
||||||
|
update_count = 0
|
||||||
|
skip_count = 0
|
||||||
|
error_count = 0
|
||||||
|
|
||||||
|
# 获取可更新的字段列表
|
||||||
|
excluded_fields = {'id', 'record_id', 'created_at', 'updated_at'}
|
||||||
|
model_fields = {f.name for f in FeishuCreator._meta.get_fields()} - excluded_fields
|
||||||
|
valid_columns = set(df.columns) & model_fields
|
||||||
|
|
||||||
|
print(f"可更新的列: {valid_columns}")
|
||||||
|
|
||||||
|
with transaction.atomic():
|
||||||
|
for index, row in df.iterrows():
|
||||||
|
try:
|
||||||
|
handle = str(row['Handle']).strip()
|
||||||
|
if not handle:
|
||||||
|
print(f"跳过第{index+2}行: Handle为空")
|
||||||
|
skip_count += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 查找现有记录
|
||||||
|
creator = FeishuCreator.objects.filter(handle=handle).first()
|
||||||
|
if not creator:
|
||||||
|
print(f"跳过Handle为'{handle}'的记录: 数据库中不存在")
|
||||||
|
skip_count += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 准备更新数据
|
||||||
|
update_data = {}
|
||||||
|
for column in valid_columns:
|
||||||
|
if column == 'Handle':
|
||||||
|
continue
|
||||||
|
|
||||||
|
value = row[column]
|
||||||
|
if pd.isna(value):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 处理特殊类型
|
||||||
|
if isinstance(value, (list, dict)):
|
||||||
|
value = json.dumps(value)
|
||||||
|
elif isinstance(value, (int, float)):
|
||||||
|
if column in ['fans_count']:
|
||||||
|
value = int(value)
|
||||||
|
else:
|
||||||
|
value = str(value)
|
||||||
|
else:
|
||||||
|
value = str(value).strip()
|
||||||
|
|
||||||
|
if value:
|
||||||
|
update_data[column] = value
|
||||||
|
|
||||||
|
# 更新记录
|
||||||
|
if update_data:
|
||||||
|
for field, value in update_data.items():
|
||||||
|
setattr(creator, field, value)
|
||||||
|
creator.save()
|
||||||
|
update_count += 1
|
||||||
|
print(f"已更新Handle为'{handle}'的记录")
|
||||||
|
else:
|
||||||
|
skip_count += 1
|
||||||
|
print(f"跳过Handle为'{handle}'的记录: 无需更新")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_count += 1
|
||||||
|
print(f"处理Handle'{handle}'时出错: {str(e)}")
|
||||||
|
|
||||||
|
print("\nExcel更新完成!统计信息:")
|
||||||
|
print(f"更新记录数:{update_count}")
|
||||||
|
print(f"跳过记录数:{skip_count}")
|
||||||
|
print(f"错误记录数:{error_count}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"处理Excel文件时出错: {str(e)}")
|
||||||
|
|
||||||
|
def sync_from_feishu():
|
||||||
|
"""从飞书多维表格同步数据"""
|
||||||
|
# 创建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("开始从飞书同步数据...")
|
||||||
|
all_records = fetch_all_records(client, APP_TOKEN, TABLE_ID, USER_ACCESS_TOKEN)
|
||||||
|
|
||||||
|
if not all_records:
|
||||||
|
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} 条记录...")
|
||||||
|
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)}")
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""主函数"""
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print("使用方法:")
|
||||||
|
print("1. 从飞书同步: python feishu.py sync")
|
||||||
|
print("2. 从Excel更新: python feishu.py excel <excel文件路径>")
|
||||||
|
return
|
||||||
|
|
||||||
|
command = sys.argv[1]
|
||||||
|
|
||||||
|
if command == 'sync':
|
||||||
|
sync_from_feishu()
|
||||||
|
elif command == 'excel':
|
||||||
|
if len(sys.argv) != 3:
|
||||||
|
print("使用方法: python feishu.py excel <excel文件路径>")
|
||||||
|
return
|
||||||
|
excel_file_path = sys.argv[2]
|
||||||
|
update_from_excel(excel_file_path)
|
||||||
|
else:
|
||||||
|
print("无效的命令。使用方法:")
|
||||||
|
print("1. 从飞书同步: python feishu.py sync")
|
||||||
|
print("2. 从Excel更新: python feishu.py excel <excel文件路径>")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
10
gmail/attachments/test2.txt
Normal file
10
gmail/attachments/test2.txt
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
本期节目内容简介
|
||||||
|
在参加各类比赛时,肯定会遇到竞争对手。英语单词 rival、opponent、competitor 和 contestant 的含义相似,都可以用来指“与他人之间存在竞争关系的人或团队”。在本集《你问我答》节目中,我们将通过和体育比赛有关的实例来为大家阐释这四个近义词之间的区别和用法。
|
||||||
|
|
||||||
|
欢迎你加入并和我们一起讨论英语学习的方方面面。请通过微博“BBC英语教学”或邮件与我们取得联系。我们的邮箱地址是 questions.chinaelt@bbc.co.uk。
|
||||||
|
|
||||||
|
文字稿
|
||||||
|
(关于台词的备注: 请注意这不是广播节目的逐字稿件。本文稿可能没有体现录制、编辑过程中对节目做出的改变。)
|
||||||
|
|
||||||
|
Feifei
|
||||||
|
大家好,欢迎收听 BBC 英语教学的《你问我答》节目,我是冯菲菲。每集节目中,我们会回答大家在英语学习时遇到的一个问题。本集的问题来自 Adela。我们来听一下她的问题。
|
1
gmail/client_secret.json
Normal file
1
gmail/client_secret.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"installed":{"client_id":"266164728215-v84lngbp3vgr4ulql01sqkg5vaigf4a5.apps.googleusercontent.com","project_id":"knowledge-454905","auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://oauth2.googleapis.com/token","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs","client_secret":"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","redirect_uris":["http://localhost"]}}
|
121
gmail/email_conversations.json
Normal file
121
gmail/email_conversations.json
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "195d5c6094b7e85f",
|
||||||
|
"subject": "这是主题",
|
||||||
|
"from": "crush wds <ardonisierni@gmail.com>",
|
||||||
|
"date": "2025-03-27 12:04:29",
|
||||||
|
"body": "你好呀\r\n",
|
||||||
|
"attachments": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "195d5c72dbafcc13",
|
||||||
|
"subject": "",
|
||||||
|
"from": "wds crush <crushwds@gmail.com>",
|
||||||
|
"date": "2025-03-27 12:05:54",
|
||||||
|
"body": "你那里天气怎么样\r\n",
|
||||||
|
"attachments": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "195d604a5ad5fabf",
|
||||||
|
"subject": "",
|
||||||
|
"from": "wds crush <crushwds@gmail.com>",
|
||||||
|
"date": "2025-03-27 13:13:03",
|
||||||
|
"body": "吃饭了吗\r\n",
|
||||||
|
"attachments": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "1960eec066745682",
|
||||||
|
"subject": "Re: 这是主题",
|
||||||
|
"from": "wds crush <crushwds@gmail.com>",
|
||||||
|
"date": "2025-04-07 14:24:28",
|
||||||
|
"body": "测试附件内容\r\n\r\n\r\ncrush wds <ardonisierni@gmail.com> 于2025年3月27日周四 12:04写道:\r\n\r\n> 你好呀\r\n>\r\n",
|
||||||
|
"attachments": [
|
||||||
|
{
|
||||||
|
"filename": "test2.txt",
|
||||||
|
"mimeType": "text/plain",
|
||||||
|
"size": 996,
|
||||||
|
"attachmentId": "ANGjdJ8O1uGve4uPqFSC2C4sMeC5jXJ3DGilhB1By705ZLGbOF30m6uITRtJHWsnB7yREKVslhYRdu4GKKvrrkWw-63ogqaZPGgi0WuSoB0OxIWXwbQSjaayUOPvc3P8y1g9A2mMAm5k0DkjH_LP0QMmulhXMg8ZgUcm4CZR7-HTBYztZSfWOoUeYkNugdzV5_2ax3GT34P5uaGHh3i4Ge6y-XDN-TDq3i1w9u_eTjZowoyVzjTj48uaQmzmFU36TQxg8ncovHk0sNZEygzCE1gbHbS1uM9N1tUHOOa6FWEpajHgz2aQwLh65SsMRqKe-LognFES-J_082IHddWs"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "1961a1981e915eb3",
|
||||||
|
"subject": "",
|
||||||
|
"from": "crush wds <ardonisierni@gmail.com>",
|
||||||
|
"date": "2025-04-09 18:29:51",
|
||||||
|
"body": "测试角色1\r\n",
|
||||||
|
"attachments": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "1961a19cc997f933",
|
||||||
|
"subject": "",
|
||||||
|
"from": "wds crush <crushwds@gmail.com>",
|
||||||
|
"date": "2025-04-09 18:30:21",
|
||||||
|
"body": "测试角色2\r\n",
|
||||||
|
"attachments": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "1961de790e0b39e6",
|
||||||
|
"subject": "",
|
||||||
|
"from": "crushwds@gmail.com",
|
||||||
|
"date": "2025-04-10 00:13:57",
|
||||||
|
"body": "您好,关于我们之前讨论的产品,我想确认一些细节...",
|
||||||
|
"attachments": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "1961dee46652dc86",
|
||||||
|
"subject": "",
|
||||||
|
"from": "crushwds@gmail.com",
|
||||||
|
"date": "2025-04-10 00:21:17",
|
||||||
|
"body": "您好,关于我们之前讨论的产品,我想确认一些细节...",
|
||||||
|
"attachments": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "1961dfd7d36bf92c",
|
||||||
|
"subject": "",
|
||||||
|
"from": "crushwds@gmail.com",
|
||||||
|
"date": "2025-04-10 00:37:54",
|
||||||
|
"body": "您好,关于我们之前讨论的产品,我想确认一些细节...",
|
||||||
|
"attachments": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "1961e39ad02da5f0",
|
||||||
|
"subject": "",
|
||||||
|
"from": "crushwds@gmail.com",
|
||||||
|
"date": "2025-04-10 01:43:38",
|
||||||
|
"body": "您好,关于我们之前讨论的产品,我想确认一些细节...",
|
||||||
|
"attachments": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "1961df3bd554a4af",
|
||||||
|
"subject": "",
|
||||||
|
"from": "crush wds <ardonisierni@gmail.com>",
|
||||||
|
"date": "2025-04-10 12:27:03",
|
||||||
|
"body": "yes\r\n",
|
||||||
|
"attachments": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "1961e3944c8aab04",
|
||||||
|
"subject": "",
|
||||||
|
"from": "crush wds <ardonisierni@gmail.com>",
|
||||||
|
"date": "2025-04-10 13:42:58",
|
||||||
|
"body": "1\r\n",
|
||||||
|
"attachments": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "1961e39660feebd9",
|
||||||
|
"subject": "",
|
||||||
|
"from": "crush wds <ardonisierni@gmail.com>",
|
||||||
|
"date": "2025-04-10 13:43:07",
|
||||||
|
"body": "2\r\n",
|
||||||
|
"attachments": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "1961ec2028c16037",
|
||||||
|
"subject": "",
|
||||||
|
"from": "crush wds <ardonisierni@gmail.com>",
|
||||||
|
"date": "2025-04-10 16:12:20",
|
||||||
|
"body": "你好\r\n",
|
||||||
|
"attachments": []
|
||||||
|
}
|
||||||
|
]
|
107
gmail/email_conversations.txt
Normal file
107
gmail/email_conversations.txt
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
==================================================
|
||||||
|
记录时间: 2025-04-10 16:52:49
|
||||||
|
==================================================
|
||||||
|
|
||||||
|
时间: 2025-03-27 12:04:29
|
||||||
|
发件人: crush wds <ardonisierni@gmail.com>
|
||||||
|
主题: 这是主题
|
||||||
|
内容:
|
||||||
|
你好呀
|
||||||
|
|
||||||
|
--------------------------------------------------
|
||||||
|
时间: 2025-03-27 12:05:54
|
||||||
|
发件人: wds crush <crushwds@gmail.com>
|
||||||
|
主题:
|
||||||
|
内容:
|
||||||
|
你那里天气怎么样
|
||||||
|
|
||||||
|
--------------------------------------------------
|
||||||
|
时间: 2025-03-27 13:13:03
|
||||||
|
发件人: wds crush <crushwds@gmail.com>
|
||||||
|
主题:
|
||||||
|
内容:
|
||||||
|
吃饭了吗
|
||||||
|
|
||||||
|
--------------------------------------------------
|
||||||
|
时间: 2025-04-07 14:24:28
|
||||||
|
发件人: wds crush <crushwds@gmail.com>
|
||||||
|
主题: Re: 这是主题
|
||||||
|
内容:
|
||||||
|
测试附件内容
|
||||||
|
|
||||||
|
|
||||||
|
crush wds <ardonisierni@gmail.com> 于2025年3月27日周四 12:04写道:
|
||||||
|
|
||||||
|
> 你好呀
|
||||||
|
>
|
||||||
|
|
||||||
|
|
||||||
|
附件:
|
||||||
|
- test2.txt (text/plain, 996 字节)
|
||||||
|
--------------------------------------------------
|
||||||
|
时间: 2025-04-09 18:29:51
|
||||||
|
发件人: crush wds <ardonisierni@gmail.com>
|
||||||
|
主题:
|
||||||
|
内容:
|
||||||
|
测试角色1
|
||||||
|
|
||||||
|
--------------------------------------------------
|
||||||
|
时间: 2025-04-09 18:30:21
|
||||||
|
发件人: wds crush <crushwds@gmail.com>
|
||||||
|
主题:
|
||||||
|
内容:
|
||||||
|
测试角色2
|
||||||
|
|
||||||
|
--------------------------------------------------
|
||||||
|
时间: 2025-04-10 00:13:57
|
||||||
|
发件人: crushwds@gmail.com
|
||||||
|
主题:
|
||||||
|
内容:
|
||||||
|
您好,关于我们之前讨论的产品,我想确认一些细节...
|
||||||
|
--------------------------------------------------
|
||||||
|
时间: 2025-04-10 00:21:17
|
||||||
|
发件人: crushwds@gmail.com
|
||||||
|
主题:
|
||||||
|
内容:
|
||||||
|
您好,关于我们之前讨论的产品,我想确认一些细节...
|
||||||
|
--------------------------------------------------
|
||||||
|
时间: 2025-04-10 00:37:54
|
||||||
|
发件人: crushwds@gmail.com
|
||||||
|
主题:
|
||||||
|
内容:
|
||||||
|
您好,关于我们之前讨论的产品,我想确认一些细节...
|
||||||
|
--------------------------------------------------
|
||||||
|
时间: 2025-04-10 01:43:38
|
||||||
|
发件人: crushwds@gmail.com
|
||||||
|
主题:
|
||||||
|
内容:
|
||||||
|
您好,关于我们之前讨论的产品,我想确认一些细节...
|
||||||
|
--------------------------------------------------
|
||||||
|
时间: 2025-04-10 12:27:03
|
||||||
|
发件人: crush wds <ardonisierni@gmail.com>
|
||||||
|
主题:
|
||||||
|
内容:
|
||||||
|
yes
|
||||||
|
|
||||||
|
--------------------------------------------------
|
||||||
|
时间: 2025-04-10 13:42:58
|
||||||
|
发件人: crush wds <ardonisierni@gmail.com>
|
||||||
|
主题:
|
||||||
|
内容:
|
||||||
|
1
|
||||||
|
|
||||||
|
--------------------------------------------------
|
||||||
|
时间: 2025-04-10 13:43:07
|
||||||
|
发件人: crush wds <ardonisierni@gmail.com>
|
||||||
|
主题:
|
||||||
|
内容:
|
||||||
|
2
|
||||||
|
|
||||||
|
--------------------------------------------------
|
||||||
|
时间: 2025-04-10 16:12:20
|
||||||
|
发件人: crush wds <ardonisierni@gmail.com>
|
||||||
|
主题:
|
||||||
|
内容:
|
||||||
|
你好
|
||||||
|
|
||||||
|
--------------------------------------------------
|
146
gmail/gmail_read.py
Normal file
146
gmail/gmail_read.py
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
'''
|
||||||
|
Reading GMAIL using Python
|
||||||
|
- Abhishek Chhibber
|
||||||
|
'''
|
||||||
|
|
||||||
|
'''
|
||||||
|
This script does the following:
|
||||||
|
- Go to Gmal inbox
|
||||||
|
- Find and read all the unread messages
|
||||||
|
- Extract details (Date, Sender, Subject, Snippet, Body) and export them to a .csv file / DB
|
||||||
|
- Mark the messages as Read - so that they are not read again
|
||||||
|
'''
|
||||||
|
|
||||||
|
'''
|
||||||
|
Before running this script, the user should get the authentication by following
|
||||||
|
the link: https://developers.google.com/gmail/api/quickstart/python
|
||||||
|
Also, credentials.json should be saved in the same directory as this file
|
||||||
|
'''
|
||||||
|
|
||||||
|
# Importing required libraries
|
||||||
|
from apiclient import discovery
|
||||||
|
from apiclient import errors
|
||||||
|
from httplib2 import Http
|
||||||
|
from oauth2client import file, client, tools
|
||||||
|
import base64
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
import dateutil.parser as parser
|
||||||
|
from datetime import datetime
|
||||||
|
import datetime
|
||||||
|
import csv
|
||||||
|
|
||||||
|
|
||||||
|
# Creating a storage.JSON file with authentication details
|
||||||
|
SCOPES = 'https://www.googleapis.com/auth/gmail.modify' # we are using modify and not readonly, as we will be marking the messages Read
|
||||||
|
store = file.Storage('storage.json')
|
||||||
|
creds = store.get()
|
||||||
|
if not creds or creds.invalid:
|
||||||
|
flow = client.flow_from_clientsecrets('credentials.json', SCOPES)
|
||||||
|
creds = tools.run_flow(flow, store)
|
||||||
|
GMAIL = discovery.build('gmail', 'v1', http=creds.authorize(Http()))
|
||||||
|
|
||||||
|
user_id = 'me'
|
||||||
|
label_id_one = 'INBOX'
|
||||||
|
label_id_two = 'UNREAD'
|
||||||
|
|
||||||
|
# Getting all the unread messages from Inbox
|
||||||
|
# labelIds can be changed accordingly
|
||||||
|
unread_msgs = GMAIL.users().messages().list(userId='me',labelIds=[label_id_one, label_id_two]).execute()
|
||||||
|
|
||||||
|
# We get a dictonary. Now reading values for the key 'messages'
|
||||||
|
mssg_list = unread_msgs['messages']
|
||||||
|
|
||||||
|
print ("Total unread messages in inbox: ", str(len(mssg_list)))
|
||||||
|
|
||||||
|
final_list = [ ]
|
||||||
|
|
||||||
|
|
||||||
|
for mssg in mssg_list:
|
||||||
|
temp_dict = { }
|
||||||
|
m_id = mssg['id'] # get id of individual message
|
||||||
|
message = GMAIL.users().messages().get(userId=user_id, id=m_id).execute() # fetch the message using API
|
||||||
|
payld = message['payload'] # get payload of the message
|
||||||
|
headr = payld['headers'] # get header of the payload
|
||||||
|
|
||||||
|
|
||||||
|
for one in headr: # getting the Subject
|
||||||
|
if one['name'] == 'Subject':
|
||||||
|
msg_subject = one['value']
|
||||||
|
temp_dict['Subject'] = msg_subject
|
||||||
|
else:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
for two in headr: # getting the date
|
||||||
|
if two['name'] == 'Date':
|
||||||
|
msg_date = two['value']
|
||||||
|
date_parse = (parser.parse(msg_date))
|
||||||
|
m_date = (date_parse.date())
|
||||||
|
temp_dict['Date'] = str(m_date)
|
||||||
|
else:
|
||||||
|
pass
|
||||||
|
|
||||||
|
for three in headr: # getting the Sender
|
||||||
|
if three['name'] == 'From':
|
||||||
|
msg_from = three['value']
|
||||||
|
temp_dict['Sender'] = msg_from
|
||||||
|
else:
|
||||||
|
pass
|
||||||
|
|
||||||
|
temp_dict['Snippet'] = message['snippet'] # fetching message snippet
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
|
||||||
|
# Fetching message body
|
||||||
|
mssg_parts = payld['parts'] # fetching the message parts
|
||||||
|
part_one = mssg_parts[0] # fetching first element of the part
|
||||||
|
part_body = part_one['body'] # fetching body of the message
|
||||||
|
part_data = part_body['data'] # fetching data from the body
|
||||||
|
clean_one = part_data.replace("-","+") # decoding from Base64 to UTF-8
|
||||||
|
clean_one = clean_one.replace("_","/") # decoding from Base64 to UTF-8
|
||||||
|
clean_two = base64.b64decode (bytes(clean_one, 'UTF-8')) # decoding from Base64 to UTF-8
|
||||||
|
soup = BeautifulSoup(clean_two , "lxml" )
|
||||||
|
mssg_body = soup.body()
|
||||||
|
# mssg_body is a readible form of message body
|
||||||
|
# depending on the end user's requirements, it can be further cleaned
|
||||||
|
# using regex, beautiful soup, or any other method
|
||||||
|
temp_dict['Message_body'] = mssg_body
|
||||||
|
|
||||||
|
except :
|
||||||
|
pass
|
||||||
|
|
||||||
|
print (temp_dict)
|
||||||
|
final_list.append(temp_dict) # This will create a dictonary item in the final list
|
||||||
|
|
||||||
|
# This will mark the messagea as read
|
||||||
|
GMAIL.users().messages().modify(userId=user_id, id=m_id,body={ 'removeLabelIds': ['UNREAD']}).execute()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
print ("Total messaged retrived: ", str(len(final_list)))
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
|
The final_list will have dictionary in the following format:
|
||||||
|
|
||||||
|
{ 'Sender': '"email.com" <name@email.com>',
|
||||||
|
'Subject': 'Lorem ipsum dolor sit ametLorem ipsum dolor sit amet',
|
||||||
|
'Date': 'yyyy-mm-dd',
|
||||||
|
'Snippet': 'Lorem ipsum dolor sit amet'
|
||||||
|
'Message_body': 'Lorem ipsum dolor sit amet'}
|
||||||
|
|
||||||
|
|
||||||
|
The dictionary can be exported as a .csv or into a databse
|
||||||
|
'''
|
||||||
|
|
||||||
|
#exporting the values as .csv
|
||||||
|
with open('CSV_NAME.csv', 'w', encoding='utf-8', newline = '') as csvfile:
|
||||||
|
fieldnames = ['Sender','Subject','Date','Snippet','Message_body']
|
||||||
|
writer = csv.DictWriter(csvfile, fieldnames=fieldnames, delimiter = ',')
|
||||||
|
writer.writeheader()
|
||||||
|
for val in final_list:
|
||||||
|
writer.writerow(val)
|
249
gmail/quickstart.py
Normal file
249
gmail/quickstart.py
Normal file
@ -0,0 +1,249 @@
|
|||||||
|
from apiclient import discovery
|
||||||
|
from httplib2 import Http
|
||||||
|
from oauth2client import file, client, tools
|
||||||
|
import base64
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
import dateutil.parser as parser
|
||||||
|
from datetime import datetime
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
|
||||||
|
# 代理设置
|
||||||
|
os.environ['HTTP_PROXY'] = 'http://127.0.0.1:7890'
|
||||||
|
os.environ['HTTPS_PROXY'] = 'http://127.0.0.1:7890'
|
||||||
|
|
||||||
|
# Gmail API 认证
|
||||||
|
SCOPES = ['https://mail.google.com/']
|
||||||
|
store = file.Storage('storage.json')
|
||||||
|
creds = store.get()
|
||||||
|
if not creds or creds.invalid:
|
||||||
|
flow = client.flow_from_clientsecrets('client_secret.json', SCOPES)
|
||||||
|
creds = tools.run_flow(flow, store)
|
||||||
|
GMAIL = discovery.build('gmail', 'v1', http=creds.authorize(Http()))
|
||||||
|
|
||||||
|
def download_attachment(message_id, attachment_id, filename):
|
||||||
|
"""下载邮件附件"""
|
||||||
|
try:
|
||||||
|
attachment = GMAIL.users().messages().attachments().get(
|
||||||
|
userId='me',
|
||||||
|
messageId=message_id,
|
||||||
|
id=attachment_id
|
||||||
|
).execute()
|
||||||
|
|
||||||
|
data = attachment['data']
|
||||||
|
file_data = base64.urlsafe_b64decode(data)
|
||||||
|
|
||||||
|
# 创建附件目录
|
||||||
|
if not os.path.exists('attachments'):
|
||||||
|
os.makedirs('attachments')
|
||||||
|
|
||||||
|
# 保存附件
|
||||||
|
filepath = os.path.join('attachments', filename)
|
||||||
|
with open(filepath, 'wb') as f:
|
||||||
|
f.write(file_data)
|
||||||
|
|
||||||
|
return filepath
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error downloading attachment: {str(e)}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_email_content(message):
|
||||||
|
"""提取邮件内容"""
|
||||||
|
try:
|
||||||
|
message_id = message['id'] # 获取邮件ID
|
||||||
|
payload = message['payload']
|
||||||
|
headers = payload['headers']
|
||||||
|
|
||||||
|
# 获取邮件基本信息
|
||||||
|
email_data = {
|
||||||
|
'id': message_id, # 保存邮件ID
|
||||||
|
'subject': '',
|
||||||
|
'from': '',
|
||||||
|
'date': '',
|
||||||
|
'body': '',
|
||||||
|
'attachments': [] # 新增附件列表
|
||||||
|
}
|
||||||
|
|
||||||
|
# 提取头部信息
|
||||||
|
for header in headers:
|
||||||
|
if header['name'] == 'Subject':
|
||||||
|
email_data['subject'] = header['value']
|
||||||
|
elif header['name'] == 'From':
|
||||||
|
email_data['from'] = header['value']
|
||||||
|
elif header['name'] == 'Date':
|
||||||
|
date = parser.parse(header['value'])
|
||||||
|
email_data['date'] = date.strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
|
||||||
|
# 定义一个递归函数来处理所有部分和附件
|
||||||
|
def process_parts(parts):
|
||||||
|
for part in parts:
|
||||||
|
# 检查是否是附件
|
||||||
|
if 'filename' in part and part['filename']:
|
||||||
|
attachment = {
|
||||||
|
'filename': part['filename'],
|
||||||
|
'mimeType': part['mimeType'],
|
||||||
|
'size': part['body'].get('size', 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
# 如果有附件内容数据,可以获取附件ID
|
||||||
|
if 'attachmentId' in part['body']:
|
||||||
|
attachment['attachmentId'] = part['body']['attachmentId']
|
||||||
|
|
||||||
|
email_data['attachments'].append(attachment)
|
||||||
|
|
||||||
|
# 处理文本内容
|
||||||
|
if part['mimeType'] == 'text/plain' and not email_data['body']:
|
||||||
|
data = part['body'].get('data', '')
|
||||||
|
if data:
|
||||||
|
text = base64.urlsafe_b64decode(data).decode('utf-8')
|
||||||
|
email_data['body'] = text
|
||||||
|
|
||||||
|
# 递归处理多部分内容
|
||||||
|
if 'parts' in part:
|
||||||
|
process_parts(part['parts'])
|
||||||
|
|
||||||
|
# 处理邮件正文和附件
|
||||||
|
if 'parts' in payload:
|
||||||
|
process_parts(payload['parts'])
|
||||||
|
elif 'body' in payload:
|
||||||
|
data = payload['body'].get('data', '')
|
||||||
|
if data:
|
||||||
|
text = base64.urlsafe_b64decode(data).decode('utf-8')
|
||||||
|
email_data['body'] = text
|
||||||
|
|
||||||
|
return email_data
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error processing email: {str(e)}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_conversations(email1, email2):
|
||||||
|
"""获取两个用户之间的所有对话"""
|
||||||
|
try:
|
||||||
|
# 构建搜索查询
|
||||||
|
query = f"from:({email1} OR {email2}) to:({email1} OR {email2})"
|
||||||
|
|
||||||
|
# 获取所有匹配的邮件
|
||||||
|
response = GMAIL.users().messages().list(userId='me', q=query).execute()
|
||||||
|
messages = []
|
||||||
|
|
||||||
|
if 'messages' in response:
|
||||||
|
messages.extend(response['messages'])
|
||||||
|
|
||||||
|
# 如果有更多页,继续获取
|
||||||
|
while 'nextPageToken' in response:
|
||||||
|
page_token = response['nextPageToken']
|
||||||
|
response = GMAIL.users().messages().list(
|
||||||
|
userId='me',
|
||||||
|
q=query,
|
||||||
|
pageToken=page_token
|
||||||
|
).execute()
|
||||||
|
messages.extend(response['messages'])
|
||||||
|
|
||||||
|
# 获取每封邮件的详细内容
|
||||||
|
conversations = []
|
||||||
|
for msg in messages:
|
||||||
|
message = GMAIL.users().messages().get(userId='me', id=msg['id']).execute()
|
||||||
|
email_data = get_email_content(message)
|
||||||
|
if email_data:
|
||||||
|
conversations.append(email_data)
|
||||||
|
|
||||||
|
# 按时间排序
|
||||||
|
conversations.sort(key=lambda x: x['date'])
|
||||||
|
|
||||||
|
return conversations
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error getting conversations: {str(e)}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
def save_conversations(conversations, output_file):
|
||||||
|
"""保存对话记录(覆盖模式)"""
|
||||||
|
try:
|
||||||
|
# 使用 'w' 模式覆盖内容
|
||||||
|
with open(output_file, 'w', encoding='utf-8') as f:
|
||||||
|
# 写入时间分割线
|
||||||
|
f.write("=" * 50 + "\n")
|
||||||
|
f.write(f"记录时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
|
||||||
|
f.write("=" * 50 + "\n\n")
|
||||||
|
|
||||||
|
# 写入对话记录
|
||||||
|
for msg in conversations:
|
||||||
|
f.write(f"时间: {msg['date']}\n")
|
||||||
|
f.write(f"发件人: {msg['from']}\n")
|
||||||
|
f.write(f"主题: {msg['subject']}\n")
|
||||||
|
f.write("内容:\n")
|
||||||
|
f.write(f"{msg['body']}\n")
|
||||||
|
|
||||||
|
# 添加附件信息
|
||||||
|
if msg['attachments']:
|
||||||
|
f.write("\n附件:\n")
|
||||||
|
for att in msg['attachments']:
|
||||||
|
f.write(f" - {att['filename']} ({att['mimeType']}, {att['size']} 字节)\n")
|
||||||
|
|
||||||
|
f.write("-" * 50 + "\n")
|
||||||
|
|
||||||
|
print(f"对话记录已保存到: {output_file}")
|
||||||
|
|
||||||
|
# 保存 JSON 格式
|
||||||
|
json_file = output_file.rsplit('.', 1)[0] + '.json'
|
||||||
|
with open(json_file, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(conversations, f, ensure_ascii=False, indent=2)
|
||||||
|
print(f"JSON 格式对话记录已保存到: {json_file}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error saving conversations: {str(e)}")
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# 设置固定的输出文件名,这样每次都会追加到同一个文件
|
||||||
|
output_file = "email_conversations.txt"
|
||||||
|
|
||||||
|
# 设置要查找的两个邮箱地址
|
||||||
|
email1 = "crushwds@gmail.com"
|
||||||
|
email2 = "ardonisierni@gmail.com"
|
||||||
|
|
||||||
|
print(f"正在获取 {email1} 和 {email2} 之间的对话...")
|
||||||
|
|
||||||
|
# 获取对话记录
|
||||||
|
conversations = get_conversations(email1, email2)
|
||||||
|
|
||||||
|
if conversations:
|
||||||
|
print(f"找到 {len(conversations)} 条对话记录")
|
||||||
|
|
||||||
|
# 统计附件
|
||||||
|
total_attachments = 0
|
||||||
|
for msg in conversations:
|
||||||
|
total_attachments += len(msg['attachments'])
|
||||||
|
|
||||||
|
# 保存对话记录
|
||||||
|
save_conversations(conversations, output_file)
|
||||||
|
|
||||||
|
# 打印对话统计
|
||||||
|
print("\n对话统计:")
|
||||||
|
print(f"总消息数: {len(conversations)}")
|
||||||
|
print(f"总附件数: {total_attachments}")
|
||||||
|
senders = {}
|
||||||
|
for msg in conversations:
|
||||||
|
sender = msg['from']
|
||||||
|
senders[sender] = senders.get(sender, 0) + 1
|
||||||
|
for sender, count in senders.items():
|
||||||
|
print(f"{sender}: {count} 条消息")
|
||||||
|
|
||||||
|
# 提示用户是否下载附件
|
||||||
|
if total_attachments > 0:
|
||||||
|
download_choice = input(f"\n发现 {total_attachments} 个附件,是否下载? (y/n): ")
|
||||||
|
if download_choice.lower() == 'y':
|
||||||
|
print("\n开始下载附件...")
|
||||||
|
downloaded = 0
|
||||||
|
for msg in conversations:
|
||||||
|
for att in msg['attachments']:
|
||||||
|
if 'attachmentId' in att:
|
||||||
|
filepath = download_attachment(msg['id'], att['attachmentId'], att['filename'])
|
||||||
|
if filepath:
|
||||||
|
downloaded += 1
|
||||||
|
print(f"已下载: {att['filename']} -> {filepath}")
|
||||||
|
print(f"\n完成! 成功下载了 {downloaded}/{total_attachments} 个附件到 'attachments' 目录")
|
||||||
|
else:
|
||||||
|
print("未找到对话记录")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
3
gmail/requirements.txt
Normal file
3
gmail/requirements.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
google-api-python-client==1.7.8
|
||||||
|
google-auth-httplib2==0.0.3
|
||||||
|
google-auth-oauthlib==0.4.0
|
1
gmail/storage.json
Normal file
1
gmail/storage.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"access_token": "ya29.a0AZYkNZga-tjDnp1lsXRohu1Tji-eVV88RaLnPjxr3HpYuBDW_6boys1aqnRnete1pT-E7ygZ5drpb0Hhbt9o15ryqbfeaKqS4HTDG_iIVvFn3npNNLSqIdvsf98burhBOnR-Nf6ty7xCsPLyFaO15bG2LybRgGL1mubVNMXSaCgYKAdQSARISFQHGX2MicVi2eoShd196_WeptFDUZg0175", "client_id": "266164728215-v84lngbp3vgr4ulql01sqkg5vaigf4a5.apps.googleusercontent.com", "client_secret": "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", "refresh_token": "1//0eAXpVapw8WjjCgYIARAAGA4SNwF-L9Irm0iHkQzqzM7Hn39nctE-DOWKTsm89Ge3nG0bfdfqloRvLMiN4YWHEKcDpLdPIuZel0Q", "token_expiry": "2025-04-10T09:51:34Z", "token_uri": "https://oauth2.googleapis.com/token", "user_agent": null, "revoke_uri": "https://oauth2.googleapis.com/revoke", "id_token": null, "id_token_jwt": null, "token_response": {"access_token": "ya29.a0AZYkNZga-tjDnp1lsXRohu1Tji-eVV88RaLnPjxr3HpYuBDW_6boys1aqnRnete1pT-E7ygZ5drpb0Hhbt9o15ryqbfeaKqS4HTDG_iIVvFn3npNNLSqIdvsf98burhBOnR-Nf6ty7xCsPLyFaO15bG2LybRgGL1mubVNMXSaCgYKAdQSARISFQHGX2MicVi2eoShd196_WeptFDUZg0175", "expires_in": 3599, "refresh_token": "1//0eAXpVapw8WjjCgYIARAAGA4SNwF-L9Irm0iHkQzqzM7Hn39nctE-DOWKTsm89Ge3nG0bfdfqloRvLMiN4YWHEKcDpLdPIuZel0Q", "scope": "https://mail.google.com/", "token_type": "Bearer", "refresh_token_expires_in": 604799}, "scopes": ["https://mail.google.com/"], "token_info_uri": "https://oauth2.googleapis.com/tokeninfo", "invalid": false, "_class": "OAuth2Credentials", "_module": "oauth2client.client"}
|
1
gmail/test.txt
Normal file
1
gmail/test.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
你好
|
10
gmail_attachments/1960eec066745682_test2.txt
Normal file
10
gmail_attachments/1960eec066745682_test2.txt
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
本期节目内容简介
|
||||||
|
在参加各类比赛时,肯定会遇到竞争对手。英语单词 rival、opponent、competitor 和 contestant 的含义相似,都可以用来指“与他人之间存在竞争关系的人或团队”。在本集《你问我答》节目中,我们将通过和体育比赛有关的实例来为大家阐释这四个近义词之间的区别和用法。
|
||||||
|
|
||||||
|
欢迎你加入并和我们一起讨论英语学习的方方面面。请通过微博“BBC英语教学”或邮件与我们取得联系。我们的邮箱地址是 questions.chinaelt@bbc.co.uk。
|
||||||
|
|
||||||
|
文字稿
|
||||||
|
(关于台词的备注: 请注意这不是广播节目的逐字稿件。本文稿可能没有体现录制、编辑过程中对节目做出的改变。)
|
||||||
|
|
||||||
|
Feifei
|
||||||
|
大家好,欢迎收听 BBC 英语教学的《你问我答》节目,我是冯菲菲。每集节目中,我们会回答大家在英语学习时遇到的一个问题。本集的问题来自 Adela。我们来听一下她的问题。
|
@ -0,0 +1 @@
|
|||||||
|
{"access_token": "ya29.a0AZYkNZga-tjDnp1lsXRohu1Tji-eVV88RaLnPjxr3HpYuBDW_6boys1aqnRnete1pT-E7ygZ5drpb0Hhbt9o15ryqbfeaKqS4HTDG_iIVvFn3npNNLSqIdvsf98burhBOnR-Nf6ty7xCsPLyFaO15bG2LybRgGL1mubVNMXSaCgYKAdQSARISFQHGX2MicVi2eoShd196_WeptFDUZg0175", "client_id": "266164728215-v84lngbp3vgr4ulql01sqkg5vaigf4a5.apps.googleusercontent.com", "client_secret": "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", "refresh_token": "1//0eAXpVapw8WjjCgYIARAAGA4SNwF-L9Irm0iHkQzqzM7Hn39nctE-DOWKTsm89Ge3nG0bfdfqloRvLMiN4YWHEKcDpLdPIuZel0Q", "token_expiry": "2025-04-10T09:51:34Z", "token_uri": "https://oauth2.googleapis.com/token", "user_agent": null, "revoke_uri": "https://oauth2.googleapis.com/revoke", "id_token": null, "id_token_jwt": null, "token_response": {"access_token": "ya29.a0AZYkNZga-tjDnp1lsXRohu1Tji-eVV88RaLnPjxr3HpYuBDW_6boys1aqnRnete1pT-E7ygZ5drpb0Hhbt9o15ryqbfeaKqS4HTDG_iIVvFn3npNNLSqIdvsf98burhBOnR-Nf6ty7xCsPLyFaO15bG2LybRgGL1mubVNMXSaCgYKAdQSARISFQHGX2MicVi2eoShd196_WeptFDUZg0175", "expires_in": 3599, "refresh_token": "1//0eAXpVapw8WjjCgYIARAAGA4SNwF-L9Irm0iHkQzqzM7Hn39nctE-DOWKTsm89Ge3nG0bfdfqloRvLMiN4YWHEKcDpLdPIuZel0Q", "scope": "https://mail.google.com/", "token_type": "Bearer", "refresh_token_expires_in": 604799}, "scopes": ["https://mail.google.com/"], "token_info_uri": "https://oauth2.googleapis.com/tokeninfo", "invalid": false, "_class": "OAuth2Credentials", "_module": "oauth2client.client"}
|
@ -1,288 +1,319 @@
|
|||||||
"""
|
"""
|
||||||
Django settings for role_based_system project.
|
Django settings for role_based_system project.
|
||||||
|
|
||||||
Generated by 'django-admin startproject' using Django 5.1.5.
|
Generated by 'django-admin startproject' using Django 5.1.5.
|
||||||
|
|
||||||
For more information on this file, see
|
For more information on this file, see
|
||||||
https://docs.djangoproject.com/en/5.1/topics/settings/
|
https://docs.djangoproject.com/en/5.1/topics/settings/
|
||||||
|
|
||||||
For the full list of settings and their values, see
|
For the full list of settings and their values, see
|
||||||
https://docs.djangoproject.com/en/5.1/ref/settings/
|
https://docs.djangoproject.com/en/5.1/ref/settings/
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
# API 配置
|
# API 配置
|
||||||
API_BASE_URL = 'http://81.69.223.133:48329'
|
API_BASE_URL = 'http://81.69.223.133:48329'
|
||||||
|
|
||||||
DEPARTMENT_GROUPS = {
|
DEPARTMENT_GROUPS = {
|
||||||
"技术部": ["开发组", "测试组", "运维组"],
|
"技术部": ["开发组", "测试组", "运维组"],
|
||||||
"产品部": ["产品组", "设计组"],
|
"产品部": ["产品组", "设计组"],
|
||||||
"市场部": ["销售组", "推广组"],
|
"市场部": ["销售组", "推广组"],
|
||||||
"行政部": ["人事组", "财务组"]
|
"行政部": ["人事组", "财务组"]
|
||||||
}
|
}
|
||||||
|
|
||||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
|
|
||||||
# Quick-start development settings - unsuitable for production
|
# Quick-start development settings - unsuitable for production
|
||||||
# See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/
|
# See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/
|
||||||
|
|
||||||
# SECURITY WARNING: keep the secret key used in production secret!
|
# SECURITY WARNING: keep the secret key used in production secret!
|
||||||
SECRET_KEY = 'django-insecure-5f*=0_2did)e(()n58=e#vd5gaf&y$thgt(h6&=p+wm1*r6mb='
|
SECRET_KEY = 'django-insecure-5f*=0_2did)e(()n58=e#vd5gaf&y$thgt(h6&=p+wm1*r6mb='
|
||||||
|
|
||||||
# SECURITY WARNING: don't run with debug turned on in production!
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
# 开发配置
|
# 开发配置
|
||||||
# DEBUG = True
|
# DEBUG = True
|
||||||
|
|
||||||
ALLOWED_HOSTS = ['*'] # 仅在开发环境使用
|
ALLOWED_HOSTS = ['*'] # 仅在开发环境使用
|
||||||
# 服务器配置
|
# 服务器配置
|
||||||
DEBUG = False
|
DEBUG = False
|
||||||
|
|
||||||
# ALLOWED_HOSTS = ['frptx.chiyong.fun', 'localhost', '127.0.0.1']
|
# ALLOWED_HOSTS = ['frptx.chiyong.fun', 'localhost', '127.0.0.1']
|
||||||
|
|
||||||
# Application definition
|
# Application definition
|
||||||
|
|
||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
'django.contrib.admin',
|
'django.contrib.admin',
|
||||||
'django.contrib.auth',
|
'django.contrib.auth',
|
||||||
'django.contrib.contenttypes',
|
'django.contrib.contenttypes',
|
||||||
'django.contrib.sessions',
|
'django.contrib.sessions',
|
||||||
'django.contrib.messages',
|
'django.contrib.messages',
|
||||||
'django.contrib.staticfiles',
|
'django.contrib.staticfiles',
|
||||||
'rest_framework',
|
'rest_framework',
|
||||||
'rest_framework.authtoken',
|
'rest_framework.authtoken',
|
||||||
'channels',
|
'channels',
|
||||||
'user_management',
|
'user_management',
|
||||||
'channels_redis',
|
'channels_redis',
|
||||||
'corsheaders',
|
'corsheaders',
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
'corsheaders.middleware.CorsMiddleware', # CORS中间件要放在CommonMiddleware前面
|
'corsheaders.middleware.CorsMiddleware', # CORS中间件要放在CommonMiddleware前面
|
||||||
'django.middleware.security.SecurityMiddleware',
|
'django.middleware.security.SecurityMiddleware',
|
||||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
'django.middleware.common.CommonMiddleware',
|
'django.middleware.common.CommonMiddleware',
|
||||||
'django.middleware.csrf.CsrfViewMiddleware', # 确保这行没有被注释
|
'django.middleware.csrf.CsrfViewMiddleware', # 确保这行没有被注释
|
||||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
'django.contrib.messages.middleware.MessageMiddleware',
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
'user_management.middleware.UserActivityMiddleware',
|
'user_management.middleware.UserActivityMiddleware',
|
||||||
]
|
]
|
||||||
|
|
||||||
ROOT_URLCONF = 'role_based_system.urls'
|
ROOT_URLCONF = 'role_based_system.urls'
|
||||||
|
|
||||||
TEMPLATES = [
|
TEMPLATES = [
|
||||||
{
|
{
|
||||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||||
'DIRS': [BASE_DIR / 'templates']
|
'DIRS': [BASE_DIR / 'templates']
|
||||||
,
|
,
|
||||||
'APP_DIRS': True,
|
'APP_DIRS': True,
|
||||||
'OPTIONS': {
|
'OPTIONS': {
|
||||||
'context_processors': [
|
'context_processors': [
|
||||||
'django.template.context_processors.debug',
|
'django.template.context_processors.debug',
|
||||||
'django.template.context_processors.request',
|
'django.template.context_processors.request',
|
||||||
'django.contrib.auth.context_processors.auth',
|
'django.contrib.auth.context_processors.auth',
|
||||||
'django.contrib.messages.context_processors.messages',
|
'django.contrib.messages.context_processors.messages',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
WSGI_APPLICATION = 'role_based_system.wsgi.application'
|
WSGI_APPLICATION = 'role_based_system.wsgi.application'
|
||||||
|
|
||||||
|
|
||||||
# Database
|
# Database
|
||||||
# https://docs.djangoproject.com/en/5.1/ref/settings/#databases
|
# https://docs.djangoproject.com/en/5.1/ref/settings/#databases
|
||||||
|
|
||||||
|
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
'default': {
|
'default': {
|
||||||
'ENGINE': 'django.db.backends.mysql',
|
'ENGINE': 'django.db.backends.mysql',
|
||||||
'NAME': 'rolebasedfilemanagement',
|
'NAME': 'rolebasedfilemanagement',
|
||||||
'USER': 'root',
|
'USER': 'root',
|
||||||
'PASSWORD': '123456',
|
'PASSWORD': '123456',
|
||||||
'HOST': 'localhost',
|
'HOST': '127.0.0.1',
|
||||||
'PORT': '3306',
|
'PORT': '3306',
|
||||||
}
|
'OPTIONS': {
|
||||||
}
|
'charset': 'utf8mb4', # 使用 utf8mb4 字符集
|
||||||
# Password validation
|
'init_command': "SET sql_mode='STRICT_TRANS_TABLES'; SET innodb_strict_mode=1; SET NAMES utf8mb4;",
|
||||||
# https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators
|
},
|
||||||
|
'TEST': {
|
||||||
AUTH_PASSWORD_VALIDATORS = [
|
'CHARSET': 'utf8mb4',
|
||||||
{
|
'COLLATION': 'utf8mb4_unicode_ci',
|
||||||
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
},
|
||||||
},
|
}
|
||||||
{
|
}
|
||||||
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
# Password validation
|
||||||
},
|
# https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators
|
||||||
{
|
|
||||||
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
AUTH_PASSWORD_VALIDATORS = [
|
||||||
},
|
{
|
||||||
{
|
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||||
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
},
|
||||||
},
|
{
|
||||||
]
|
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||||
LANGUAGE_CODE = 'en-us'
|
},
|
||||||
|
{
|
||||||
TIME_ZONE = 'UTC'
|
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||||
|
},
|
||||||
USE_I18N = True
|
]
|
||||||
|
|
||||||
USE_TZ = True
|
|
||||||
|
|
||||||
|
LANGUAGE_CODE = 'en-us'
|
||||||
|
|
||||||
STATIC_URL = 'static/'
|
TIME_ZONE = 'Asia/Shanghai'
|
||||||
|
|
||||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
USE_I18N = True
|
||||||
|
|
||||||
AUTH_USER_MODEL = 'user_management.User'
|
USE_TZ = True # 将此项设置为True以启用时区支持
|
||||||
|
|
||||||
REST_FRAMEWORK = {
|
|
||||||
'DEFAULT_AUTHENTICATION_CLASSES': [
|
|
||||||
'rest_framework.authentication.TokenAuthentication',
|
STATIC_URL = 'static/'
|
||||||
'rest_framework.authentication.SessionAuthentication',
|
|
||||||
],
|
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||||
'DEFAULT_PERMISSION_CLASSES': [
|
|
||||||
'rest_framework.permissions.IsAuthenticated',
|
AUTH_USER_MODEL = 'user_management.User'
|
||||||
]
|
|
||||||
}
|
REST_FRAMEWORK = {
|
||||||
|
'DEFAULT_AUTHENTICATION_CLASSES': [
|
||||||
# Channels 配置
|
'rest_framework.authentication.TokenAuthentication',
|
||||||
ASGI_APPLICATION = "role_based_system.asgi.application"
|
'rest_framework.authentication.SessionAuthentication',
|
||||||
|
],
|
||||||
# Channel Layers 配置
|
'DEFAULT_PERMISSION_CLASSES': [
|
||||||
CHANNEL_LAYERS = {
|
'rest_framework.permissions.IsAuthenticated',
|
||||||
"default": {
|
]
|
||||||
"BACKEND": "channels_redis.core.RedisChannelLayer",
|
}
|
||||||
"CONFIG": {
|
|
||||||
"hosts": [("127.0.0.1", 6379)],
|
# Channels 配置
|
||||||
"capacity": 1500, # 消息队列容量
|
ASGI_APPLICATION = "role_based_system.asgi.application"
|
||||||
"expiry": 10, # 消息过期时间(秒)
|
|
||||||
},
|
# Channel Layers 配置
|
||||||
},
|
CHANNEL_LAYERS = {
|
||||||
}
|
"default": {
|
||||||
|
"BACKEND": "channels_redis.core.RedisChannelLayer",
|
||||||
|
"CONFIG": {
|
||||||
# CORS 配置
|
"hosts": [("127.0.0.1", 6379)],
|
||||||
CORS_ALLOW_ALL_ORIGINS = True
|
"capacity": 1500, # 消息队列容量
|
||||||
CORS_ALLOW_CREDENTIALS = True
|
"expiry": 10, # 消息过期时间(秒)
|
||||||
CORS_ALLOWED_ORIGINS = [
|
},
|
||||||
"http://localhost:8000",
|
},
|
||||||
"http://127.0.0.1:8000",
|
}
|
||||||
"http://124.222.236.141:8000",
|
|
||||||
"ws://localhost:8000", # 添加 WebSocket
|
|
||||||
"ws://127.0.0.1:8000", # 添加 WebSocket
|
# CORS 配置
|
||||||
"ws://124.222.236.141:8000", # 添加 WebSocket
|
CORS_ALLOW_ALL_ORIGINS = True
|
||||||
]
|
CORS_ALLOW_CREDENTIALS = True
|
||||||
# 允许的请求头
|
CORS_ALLOWED_ORIGINS = [
|
||||||
CORS_ALLOWED_HEADERS = [
|
"http://localhost:8000",
|
||||||
'accept',
|
"http://127.0.0.1:8000",
|
||||||
'accept-encoding',
|
"http://124.222.236.141:8000",
|
||||||
'authorization',
|
"ws://localhost:8000", # 添加 WebSocket
|
||||||
'content-type',
|
"ws://127.0.0.1:8000", # 添加 WebSocket
|
||||||
'dnt',
|
"ws://124.222.236.141:8000", # 添加 WebSocket
|
||||||
'origin',
|
]
|
||||||
'user-agent',
|
# 允许的请求头
|
||||||
'x-csrftoken',
|
CORS_ALLOWED_HEADERS = [
|
||||||
'x-requested-with',
|
'accept',
|
||||||
]
|
'accept-encoding',
|
||||||
|
'authorization',
|
||||||
# 允许的请求方法
|
'content-type',
|
||||||
CORS_ALLOWED_METHODS = [
|
'dnt',
|
||||||
'DELETE',
|
'origin',
|
||||||
'GET',
|
'user-agent',
|
||||||
'OPTIONS',
|
'x-csrftoken',
|
||||||
'PATCH',
|
'x-requested-with',
|
||||||
'POST',
|
]
|
||||||
'PUT',
|
|
||||||
]
|
# 允许的请求方法
|
||||||
|
CORS_ALLOWED_METHODS = [
|
||||||
|
'DELETE',
|
||||||
|
'GET',
|
||||||
# WebSocket 允许的来源
|
'OPTIONS',
|
||||||
CSRF_TRUSTED_ORIGINS = [
|
'PATCH',
|
||||||
'http://localhost:8000',
|
'POST',
|
||||||
'http://127.0.0.1:8000',
|
'PUT',
|
||||||
'http://124.222.236.141:8000',
|
]
|
||||||
'ws://localhost:8000', # 添加 WebSocket
|
|
||||||
'ws://127.0.0.1:8000', # 添加 WebSocket
|
|
||||||
'ws://124.222.236.141:8000', # 添加 WebSocket
|
|
||||||
]
|
# WebSocket 允许的来源
|
||||||
# 服务器配置
|
CSRF_TRUSTED_ORIGINS = [
|
||||||
# 静态文件配置
|
'http://localhost:8000',
|
||||||
STATIC_URL = '/static/'
|
'http://127.0.0.1:8000',
|
||||||
STATIC_ROOT = os.path.join(BASE_DIR, 'static')
|
'http://124.222.236.141:8000',
|
||||||
|
'ws://localhost:8000', # 添加 WebSocket
|
||||||
# 媒体文件配置
|
'ws://127.0.0.1:8000', # 添加 WebSocket
|
||||||
MEDIA_URL = '/media/'
|
'ws://124.222.236.141:8000', # 添加 WebSocket
|
||||||
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
|
]
|
||||||
|
# 服务器配置
|
||||||
# 文件上传配置
|
# 静态文件配置
|
||||||
FILE_UPLOAD_MAX_MEMORY_SIZE = 10 * 1024 * 1024 # 10MB
|
STATIC_URL = '/static/'
|
||||||
MAX_UPLOAD_SIZE = 10 * 1024 * 1024 # 10MB
|
STATIC_ROOT = os.path.join(BASE_DIR, 'static')
|
||||||
|
|
||||||
# 日志配置
|
# 媒体文件配置
|
||||||
LOGGING = {
|
MEDIA_URL = '/media/'
|
||||||
'version': 1,
|
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
|
||||||
'disable_existing_loggers': False,
|
|
||||||
'handlers': {
|
# 文件上传配置
|
||||||
'console': {
|
FILE_UPLOAD_MAX_MEMORY_SIZE = 10 * 1024 * 1024 # 10MB
|
||||||
'class': 'logging.StreamHandler',
|
MAX_UPLOAD_SIZE = 10 * 1024 * 1024 # 10MB
|
||||||
},
|
|
||||||
'file': {
|
# 日志配置
|
||||||
'class': 'logging.FileHandler',
|
LOGGING = {
|
||||||
'filename': 'debug.log',
|
'version': 1,
|
||||||
},
|
'disable_existing_loggers': False,
|
||||||
},
|
'handlers': {
|
||||||
'root': {
|
'console': {
|
||||||
'handlers': ['console', 'file'],
|
'class': 'logging.StreamHandler',
|
||||||
'level': 'DEBUG',
|
},
|
||||||
},
|
'file': {
|
||||||
'loggers': {
|
'class': 'logging.FileHandler',
|
||||||
'django.security.csrf': {
|
'filename': 'debug.log',
|
||||||
'handlers': ['file'],
|
},
|
||||||
'level': 'WARNING',
|
},
|
||||||
'propagate': True,
|
'root': {
|
||||||
},
|
'handlers': ['console', 'file'],
|
||||||
},
|
'level': 'DEBUG',
|
||||||
}
|
},
|
||||||
|
'loggers': {
|
||||||
# CSRF 配置
|
'django.security.csrf': {
|
||||||
CSRF_COOKIE_SECURE = False # 开发环境设置为 False
|
'handlers': ['file'],
|
||||||
CSRF_COOKIE_HTTPONLY = False
|
'level': 'WARNING',
|
||||||
CSRF_USE_SESSIONS = False
|
'propagate': True,
|
||||||
CSRF_COOKIE_SAMESITE = 'Lax'
|
},
|
||||||
CSRF_TRUSTED_ORIGINS = [
|
},
|
||||||
'http://localhost:8000',
|
}
|
||||||
'http://127.0.0.1:8000',
|
|
||||||
'ws://localhost:8000', # 添加 WebSocket
|
# CSRF 配置
|
||||||
'ws://127.0.0.1:8000' # 添加 WebSocket
|
CSRF_COOKIE_SECURE = False # 开发环境设置为 False
|
||||||
]
|
CSRF_COOKIE_HTTPONLY = False
|
||||||
|
CSRF_USE_SESSIONS = False
|
||||||
# Session 配置
|
CSRF_COOKIE_SAMESITE = 'Lax'
|
||||||
SESSION_COOKIE_SECURE = False # 开发环境设置为 False
|
CSRF_TRUSTED_ORIGINS = [
|
||||||
SESSION_COOKIE_HTTPONLY = True
|
'http://localhost:8000',
|
||||||
SESSION_COOKIE_SAMESITE = 'Lax'
|
'http://127.0.0.1:8000',
|
||||||
|
'ws://localhost:8000', # 添加 WebSocket
|
||||||
# REST Framework 配置
|
'ws://127.0.0.1:8000' # 添加 WebSocket
|
||||||
REST_FRAMEWORK = {
|
]
|
||||||
'DEFAULT_AUTHENTICATION_CLASSES': [
|
|
||||||
'rest_framework.authentication.TokenAuthentication',
|
# Session 配置
|
||||||
'rest_framework.authentication.SessionAuthentication', # WebSocket 需要
|
SESSION_COOKIE_SECURE = False # 开发环境设置为 False
|
||||||
],
|
SESSION_COOKIE_HTTPONLY = True
|
||||||
'DEFAULT_PERMISSION_CLASSES': [
|
SESSION_COOKIE_SAMESITE = 'Lax'
|
||||||
'rest_framework.permissions.IsAuthenticated',
|
|
||||||
],
|
# REST Framework 配置
|
||||||
'DEFAULT_PARSER_CLASSES': [
|
REST_FRAMEWORK = {
|
||||||
'rest_framework.parsers.JSONParser',
|
'DEFAULT_AUTHENTICATION_CLASSES': [
|
||||||
'rest_framework.parsers.FormParser',
|
'rest_framework.authentication.TokenAuthentication',
|
||||||
'rest_framework.parsers.MultiPartParser'
|
'rest_framework.authentication.SessionAuthentication', # WebSocket 需要
|
||||||
],
|
],
|
||||||
}
|
'DEFAULT_PERMISSION_CLASSES': [
|
||||||
|
'rest_framework.permissions.IsAuthenticated',
|
||||||
|
],
|
||||||
|
'DEFAULT_PARSER_CLASSES': [
|
||||||
|
'rest_framework.parsers.JSONParser',
|
||||||
|
'rest_framework.parsers.FormParser',
|
||||||
|
'rest_framework.parsers.MultiPartParser'
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
# Gmail API配置
|
||||||
|
GOOGLE_CLOUD_PROJECT = 'knowledge-454905' # 更新为当前使用的项目ID
|
||||||
|
GMAIL_API_SCOPES = ['https://mail.google.com/']
|
||||||
|
GMAIL_TOPIC_NAME = 'gmail-watch-topic'
|
||||||
|
|
||||||
|
# Gmail webhook地址 (开发环境使用本机内网穿透地址)
|
||||||
|
GMAIL_WEBHOOK_URL = 'https://a7a4-116-227-35-74.ngrok-free.app/api/user/gmail/webhook/'
|
||||||
|
|
||||||
|
# 如果在生产环境,使用以下固定地址
|
||||||
|
# GMAIL_WEBHOOK_URL = 'https://你的域名/api/user/gmail/webhook/'
|
||||||
|
|
||||||
|
# 媒体文件目录
|
||||||
|
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
|
||||||
|
MEDIA_URL = '/media/'
|
||||||
|
|
||||||
|
# 时区设置
|
||||||
|
TIME_ZONE = 'Asia/Shanghai'
|
||||||
|
USE_I18N = True
|
||||||
|
USE_L10N = True
|
||||||
|
USE_TZ = True # 将此项设置为True以启用时区支持
|
||||||
|
|
||||||
|
|
||||||
|
@ -18,6 +18,7 @@ from django.contrib import admin
|
|||||||
from django.urls import path, include
|
from django.urls import path, include
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.conf.urls.static import static
|
from django.conf.urls.static import static
|
||||||
|
from user_management.views import gmail_webhook # 直接导入视图函数
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
# 管理后台
|
# 管理后台
|
||||||
@ -26,6 +27,10 @@ urlpatterns = [
|
|||||||
# API路由
|
# API路由
|
||||||
path('api/', include('user_management.urls')),
|
path('api/', include('user_management.urls')),
|
||||||
|
|
||||||
|
# 专用Gmail Webhook路由 - 直接匹配根路径
|
||||||
|
path('api/user/gmail/webhook/', gmail_webhook, name='root_gmail_webhook'), # 修改为正确路径
|
||||||
|
path('gmail/webhook/', gmail_webhook, name='alt_gmail_webhook'), # 添加备用路径
|
||||||
|
|
||||||
# 媒体文件服务
|
# 媒体文件服务
|
||||||
*static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT),
|
*static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT),
|
||||||
|
|
||||||
@ -34,8 +39,8 @@ urlpatterns = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
# 添加调试工具栏(仅在DEBUG模式下)
|
# 添加调试工具栏(仅在DEBUG模式下)
|
||||||
if settings.DEBUG:
|
# if settings.DEBUG:
|
||||||
import debug_toolbar
|
# import debug_toolbar
|
||||||
urlpatterns = [
|
# urlpatterns = [
|
||||||
path('__debug__/', include(debug_toolbar.urls)),
|
# path('__debug__/', include(debug_toolbar.urls)),
|
||||||
] + urlpatterns
|
# ] + urlpatterns
|
||||||
|
1
temp_client_secret.json
Normal file
1
temp_client_secret.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"installed": {"client_id": "240872828479-570luspoc31259l1faab6kmjmpcsa9n9.apps.googleusercontent.com", "project_id": "first-renderer-454910-c1", "auth_uri": "https://accounts.google.com/o/oauth2/auth", "token_uri": "https://oauth2.googleapis.com/token", "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", "client_secret": "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", "redirect_uris": ["http://localhost"]}}
|
2187
user_management/gmail_integration.py
Normal file
2187
user_management/gmail_integration.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1 @@
|
|||||||
|
# 管理命令包
|
@ -0,0 +1 @@
|
|||||||
|
# Gmail管理命令
|
@ -0,0 +1,80 @@
|
|||||||
|
from django.core.management.base import BaseCommand, CommandError
|
||||||
|
from user_management.models import GmailCredential, User
|
||||||
|
from user_management.gmail_integration import GmailIntegration
|
||||||
|
import logging
|
||||||
|
import pickle
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = '更新Gmail凭证中的邮箱信息'
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument('--email', type=str, help='指定用户邮箱')
|
||||||
|
parser.add_argument('--gmail', type=str, help='指定要设置的Gmail邮箱')
|
||||||
|
parser.add_argument('--all', action='store_true', help='更新所有凭证')
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
email = options.get('email')
|
||||||
|
gmail = options.get('gmail')
|
||||||
|
update_all = options.get('all')
|
||||||
|
|
||||||
|
if update_all:
|
||||||
|
# 更新所有凭证
|
||||||
|
credentials = GmailCredential.objects.filter(is_active=True)
|
||||||
|
self.stdout.write(f"找到 {credentials.count()} 个活跃的Gmail凭证")
|
||||||
|
|
||||||
|
for credential in credentials:
|
||||||
|
self._update_credential(credential, gmail)
|
||||||
|
elif email:
|
||||||
|
# 更新指定用户的凭证
|
||||||
|
try:
|
||||||
|
user = User.objects.get(email=email)
|
||||||
|
credentials = GmailCredential.objects.filter(user=user, is_active=True)
|
||||||
|
|
||||||
|
if not credentials.exists():
|
||||||
|
raise CommandError(f"未找到用户 {email} 的Gmail凭证")
|
||||||
|
|
||||||
|
for credential in credentials:
|
||||||
|
self._update_credential(credential, gmail)
|
||||||
|
except User.DoesNotExist:
|
||||||
|
raise CommandError(f"未找到用户 {email}")
|
||||||
|
else:
|
||||||
|
self.stdout.write("请提供--email参数或--all参数")
|
||||||
|
|
||||||
|
def _update_credential(self, credential, gmail=None):
|
||||||
|
"""更新单个凭证"""
|
||||||
|
user = credential.user
|
||||||
|
self.stdout.write(f"正在更新用户 {user.email} 的Gmail凭证...")
|
||||||
|
|
||||||
|
if gmail:
|
||||||
|
# 如果指定了Gmail邮箱,直接使用
|
||||||
|
credential.gmail_email = gmail
|
||||||
|
credential.save()
|
||||||
|
self.stdout.write(self.style.SUCCESS(f"已手动设置Gmail邮箱为: {gmail}"))
|
||||||
|
return
|
||||||
|
|
||||||
|
# 尝试使用API获取Gmail邮箱
|
||||||
|
try:
|
||||||
|
# 从凭证数据中恢复服务
|
||||||
|
creds = pickle.loads(credential.credentials)
|
||||||
|
|
||||||
|
if creds and not creds.invalid:
|
||||||
|
# 创建Gmail集成实例
|
||||||
|
integration = GmailIntegration(user=user)
|
||||||
|
integration.credentials = creds
|
||||||
|
|
||||||
|
# 尝试调用API获取用户资料
|
||||||
|
profile = integration.gmail_service.users().getProfile(userId='me').execute()
|
||||||
|
gmail_email = profile.get('emailAddress')
|
||||||
|
|
||||||
|
if gmail_email:
|
||||||
|
credential.gmail_email = gmail_email
|
||||||
|
credential.save()
|
||||||
|
self.stdout.write(self.style.SUCCESS(f"已更新Gmail邮箱为: {gmail_email}"))
|
||||||
|
else:
|
||||||
|
self.stdout.write(self.style.WARNING("无法从API获取Gmail邮箱"))
|
||||||
|
else:
|
||||||
|
self.stdout.write(self.style.ERROR("凭证无效,请重新授权"))
|
||||||
|
except Exception as e:
|
||||||
|
self.stdout.write(self.style.ERROR(f"更新失败: {str(e)}"))
|
@ -1,4 +1,4 @@
|
|||||||
# Generated by Django 5.1.5 on 2025-02-26 09:23
|
# Generated by Django 5.1.5 on 2025-03-28 06:51
|
||||||
|
|
||||||
import django.contrib.auth.models
|
import django.contrib.auth.models
|
||||||
import django.contrib.auth.validators
|
import django.contrib.auth.validators
|
||||||
@ -18,6 +18,43 @@ class Migration(migrations.Migration):
|
|||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='FeishuCreator',
|
||||||
|
fields=[
|
||||||
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||||
|
('record_id', models.CharField(max_length=100, unique=True, verbose_name='飞书记录ID')),
|
||||||
|
('contact_person', models.CharField(blank=True, max_length=50, verbose_name='对接人')),
|
||||||
|
('handle', models.CharField(blank=True, max_length=100, verbose_name='Handle')),
|
||||||
|
('tiktok_url', models.URLField(blank=True, verbose_name='链接')),
|
||||||
|
('fans_count', models.CharField(blank=True, max_length=50, verbose_name='粉丝数')),
|
||||||
|
('gmv', models.CharField(blank=True, max_length=50, verbose_name='GMV')),
|
||||||
|
('email', models.EmailField(blank=True, max_length=254, verbose_name='邮箱')),
|
||||||
|
('phone', models.CharField(blank=True, max_length=50, verbose_name='手机号|WhatsApp')),
|
||||||
|
('account_type', models.CharField(blank=True, max_length=50, verbose_name='账号属性')),
|
||||||
|
('price_quote', models.CharField(blank=True, max_length=100, verbose_name='报价')),
|
||||||
|
('response_speed', models.CharField(blank=True, max_length=50, verbose_name='回复速度')),
|
||||||
|
('cooperation_intention', models.CharField(blank=True, max_length=50, verbose_name='合作意向')),
|
||||||
|
('payment_method', models.CharField(blank=True, max_length=50, verbose_name='支付方式')),
|
||||||
|
('payment_account', models.CharField(blank=True, max_length=100, verbose_name='收款账号')),
|
||||||
|
('address', models.TextField(blank=True, verbose_name='收件地址')),
|
||||||
|
('has_ooin', models.CharField(blank=True, max_length=10, verbose_name='签约OOIN?')),
|
||||||
|
('source', models.CharField(blank=True, max_length=100, verbose_name='渠道来源')),
|
||||||
|
('contact_status', models.CharField(blank=True, max_length=50, verbose_name='建联进度')),
|
||||||
|
('cooperation_brands', models.JSONField(blank=True, default=list, verbose_name='合作品牌')),
|
||||||
|
('system_categories', models.CharField(blank=True, max_length=100, verbose_name='系统展示的带货品类')),
|
||||||
|
('actual_categories', models.CharField(blank=True, max_length=100, verbose_name='实际高播放量带货品类')),
|
||||||
|
('human_categories', models.CharField(blank=True, max_length=100, verbose_name='达人标想要货品类')),
|
||||||
|
('creator_base', models.CharField(blank=True, max_length=100, verbose_name='达人base')),
|
||||||
|
('notes', models.TextField(blank=True, verbose_name='父记录')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': '创作者数据',
|
||||||
|
'verbose_name_plural': '创作者数据',
|
||||||
|
'db_table': 'feishu_creators',
|
||||||
|
},
|
||||||
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='User',
|
name='User',
|
||||||
fields=[
|
fields=[
|
||||||
@ -73,79 +110,22 @@ class Migration(migrations.Migration):
|
|||||||
name='KnowledgeBase',
|
name='KnowledgeBase',
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||||
('name', models.CharField(max_length=100, verbose_name='知识库名称')),
|
('user_id', models.UUIDField(verbose_name='创建者ID')),
|
||||||
|
('name', models.CharField(max_length=100, unique=True, verbose_name='知识库名称')),
|
||||||
('desc', models.TextField(blank=True, null=True, verbose_name='知识库描述')),
|
('desc', models.TextField(blank=True, null=True, verbose_name='知识库描述')),
|
||||||
('type', models.CharField(choices=[('admin', '管理级知识库'), ('leader', '部门级知识库'), ('member', '成员级知识库'), ('private', '私有知识库'), ('secret', '公司级别私密知识库')], default='private', max_length=20, verbose_name='知识库类型')),
|
('type', models.CharField(choices=[('admin', '管理级知识库'), ('leader', '部门级知识库'), ('member', '成员级知识库'), ('private', '私有知识库'), ('secret', '公司级别私密知识库')], default='private', max_length=20, verbose_name='知识库类型')),
|
||||||
('department', models.CharField(blank=True, max_length=50, null=True)),
|
('department', models.CharField(blank=True, max_length=50, null=True)),
|
||||||
('group', models.CharField(blank=True, max_length=50, null=True)),
|
('group', models.CharField(blank=True, max_length=50, null=True)),
|
||||||
('user_id', models.CharField(max_length=50, verbose_name='创建者ID')),
|
|
||||||
('documents', models.JSONField(default=list)),
|
('documents', models.JSONField(default=list)),
|
||||||
('char_length', models.IntegerField(default=0)),
|
('char_length', models.IntegerField(default=0)),
|
||||||
('document_count', models.IntegerField(default=0)),
|
('document_count', models.IntegerField(default=0)),
|
||||||
('external_id', models.UUIDField(blank=True, null=True)),
|
('external_id', models.UUIDField(blank=True, null=True)),
|
||||||
('create_time', models.DateTimeField(auto_now_add=True)),
|
('create_time', models.DateTimeField(auto_now_add=True)),
|
||||||
('update_time', models.DateTimeField(auto_now=True)),
|
('update_time', models.DateTimeField(auto_now=True)),
|
||||||
('owners', models.ManyToManyField(related_name='owned_knowledge_bases', to=settings.AUTH_USER_MODEL, verbose_name='所有者')),
|
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'db_table': 'knowledge_bases',
|
'db_table': 'knowledge_bases',
|
||||||
},
|
'indexes': [models.Index(fields=['type'], name='knowledge_b_type_0439e7_idx'), models.Index(fields=['department'], name='knowledge_b_departm_e739fd_idx'), models.Index(fields=['group'], name='knowledge_b_group_3dcf34_idx')],
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='ChatHistory',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('conversation_id', models.CharField(db_index=True, max_length=100)),
|
|
||||||
('parent_id', models.CharField(blank=True, max_length=100, null=True)),
|
|
||||||
('role', models.CharField(choices=[('user', '用户'), ('assistant', 'AI助手'), ('system', '系统')], max_length=20)),
|
|
||||||
('content', models.TextField()),
|
|
||||||
('tokens', models.IntegerField(default=0, help_text='消息token数')),
|
|
||||||
('metadata', models.JSONField(blank=True, default=dict)),
|
|
||||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
|
||||||
('is_deleted', models.BooleanField(default=False)),
|
|
||||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
|
||||||
('knowledge_base', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='user_management.knowledgebase')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'db_table': 'chat_history',
|
|
||||||
'ordering': ['created_at'],
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='KnowledgeBasePermission',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('can_read', models.BooleanField(default=False, verbose_name='查看权限')),
|
|
||||||
('can_edit', models.BooleanField(default=False, verbose_name='修改权限')),
|
|
||||||
('can_delete', models.BooleanField(default=False, verbose_name='删除权限')),
|
|
||||||
('status', models.CharField(choices=[('active', '生效中'), ('expired', '已过期'), ('revoked', '已撤销')], default='active', max_length=10, verbose_name='状态')),
|
|
||||||
('granted_at', models.DateTimeField(auto_now_add=True, verbose_name='授权时间')),
|
|
||||||
('expires_at', models.DateTimeField(blank=True, null=True, verbose_name='过期时间')),
|
|
||||||
('granted_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='granted_permissions', to=settings.AUTH_USER_MODEL, verbose_name='授权人')),
|
|
||||||
('knowledge_base', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_permissions', to='user_management.knowledgebase', verbose_name='知识库')),
|
|
||||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='knowledge_base_permissions', to=settings.AUTH_USER_MODEL, verbose_name='用户')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': '知识库权限',
|
|
||||||
'verbose_name_plural': '知识库权限',
|
|
||||||
'db_table': 'knowledge_base_permissions',
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Notification',
|
|
||||||
fields=[
|
|
||||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
|
||||||
('type', models.CharField(choices=[('permission_request', '权限申请'), ('permission_approved', '权限批准'), ('permission_rejected', '权限拒绝'), ('permission_expired', '权限过期'), ('system_notice', '系统通知')], max_length=20)),
|
|
||||||
('title', models.CharField(max_length=100)),
|
|
||||||
('content', models.TextField()),
|
|
||||||
('is_read', models.BooleanField(default=False)),
|
|
||||||
('related_resource', models.CharField(blank=True, max_length=100)),
|
|
||||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
|
||||||
('receiver', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='received_notifications', to=settings.AUTH_USER_MODEL)),
|
|
||||||
('sender', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sent_notifications', to=settings.AUTH_USER_MODEL)),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'ordering': ['-created_at'],
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
@ -181,40 +161,65 @@ class Migration(migrations.Migration):
|
|||||||
'db_table': 'user_profiles',
|
'db_table': 'user_profiles',
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
migrations.AddIndex(
|
migrations.CreateModel(
|
||||||
model_name='knowledgebase',
|
name='ChatHistory',
|
||||||
index=models.Index(fields=['type'], name='knowledge_b_type_0439e7_idx'),
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('conversation_id', models.CharField(db_index=True, max_length=100)),
|
||||||
|
('parent_id', models.CharField(blank=True, max_length=100, null=True)),
|
||||||
|
('role', models.CharField(choices=[('user', '用户'), ('assistant', 'AI助手'), ('system', '系统')], max_length=20)),
|
||||||
|
('content', models.TextField()),
|
||||||
|
('tokens', models.IntegerField(default=0, help_text='消息token数')),
|
||||||
|
('metadata', models.JSONField(blank=True, default=dict, help_text="\n {\n 'model_id': 'xxx',\n 'dataset_id_list': ['id1', 'id2', ...],\n 'dataset_external_id_list': ['ext1', 'ext2', ...],\n 'primary_knowledge_base': 'id1'\n }\n ")),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('is_deleted', models.BooleanField(default=False)),
|
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||||
|
('knowledge_base', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='user_management.knowledgebase')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'db_table': 'chat_history',
|
||||||
|
'ordering': ['created_at'],
|
||||||
|
'indexes': [models.Index(fields=['conversation_id', 'created_at'], name='chat_histor_convers_33721a_idx'), models.Index(fields=['user', 'created_at'], name='chat_histor_user_id_aa050a_idx'), models.Index(fields=['conversation_id', 'is_deleted'], name='chat_histor_convers_89bc43_idx')],
|
||||||
|
},
|
||||||
),
|
),
|
||||||
migrations.AddIndex(
|
migrations.CreateModel(
|
||||||
model_name='knowledgebase',
|
name='KnowledgeBasePermission',
|
||||||
index=models.Index(fields=['department'], name='knowledge_b_departm_e739fd_idx'),
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('can_read', models.BooleanField(default=False, verbose_name='查看权限')),
|
||||||
|
('can_edit', models.BooleanField(default=False, verbose_name='修改权限')),
|
||||||
|
('can_delete', models.BooleanField(default=False, verbose_name='删除权限')),
|
||||||
|
('status', models.CharField(choices=[('active', '生效中'), ('expired', '已过期'), ('revoked', '已撤销')], default='active', max_length=10, verbose_name='状态')),
|
||||||
|
('granted_at', models.DateTimeField(auto_now_add=True, verbose_name='授权时间')),
|
||||||
|
('expires_at', models.DateTimeField(blank=True, null=True, verbose_name='过期时间')),
|
||||||
|
('granted_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='granted_permissions', to=settings.AUTH_USER_MODEL, verbose_name='授权人')),
|
||||||
|
('knowledge_base', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_permissions', to='user_management.knowledgebase', verbose_name='知识库')),
|
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='knowledge_base_permissions', to=settings.AUTH_USER_MODEL, verbose_name='用户')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': '知识库权限',
|
||||||
|
'verbose_name_plural': '知识库权限',
|
||||||
|
'db_table': 'knowledge_base_permissions',
|
||||||
|
'indexes': [models.Index(fields=['knowledge_base', 'user', 'status'], name='knowledge_b_knowled_88e81e_idx')],
|
||||||
|
'unique_together': {('knowledge_base', 'user')},
|
||||||
|
},
|
||||||
),
|
),
|
||||||
migrations.AddIndex(
|
migrations.CreateModel(
|
||||||
model_name='knowledgebase',
|
name='Notification',
|
||||||
index=models.Index(fields=['group'], name='knowledge_b_group_3dcf34_idx'),
|
fields=[
|
||||||
),
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||||
migrations.AddIndex(
|
('type', models.CharField(choices=[('permission_request', '权限申请'), ('permission_approved', '权限批准'), ('permission_rejected', '权限拒绝'), ('permission_expired', '权限过期'), ('system_notice', '系统通知')], max_length=20)),
|
||||||
model_name='chathistory',
|
('title', models.CharField(max_length=100)),
|
||||||
index=models.Index(fields=['conversation_id', 'created_at'], name='chat_histor_convers_33721a_idx'),
|
('content', models.TextField()),
|
||||||
),
|
('is_read', models.BooleanField(default=False)),
|
||||||
migrations.AddIndex(
|
('related_resource', models.CharField(blank=True, max_length=100)),
|
||||||
model_name='chathistory',
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
index=models.Index(fields=['user', 'created_at'], name='chat_histor_user_id_aa050a_idx'),
|
('receiver', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='received_notifications', to=settings.AUTH_USER_MODEL)),
|
||||||
),
|
('sender', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sent_notifications', to=settings.AUTH_USER_MODEL)),
|
||||||
migrations.AddIndex(
|
],
|
||||||
model_name='knowledgebasepermission',
|
options={
|
||||||
index=models.Index(fields=['knowledge_base', 'user', 'status'], name='knowledge_b_knowled_88e81e_idx'),
|
'ordering': ['-created_at'],
|
||||||
),
|
'indexes': [models.Index(fields=['receiver', '-created_at'], name='user_manage_receive_fcb0eb_idx'), models.Index(fields=['type', 'is_read'], name='user_manage_type_362052_idx')],
|
||||||
migrations.AlterUniqueTogether(
|
},
|
||||||
name='knowledgebasepermission',
|
|
||||||
unique_together={('knowledge_base', 'user')},
|
|
||||||
),
|
|
||||||
migrations.AddIndex(
|
|
||||||
model_name='notification',
|
|
||||||
index=models.Index(fields=['receiver', '-created_at'], name='user_manage_receive_fcb0eb_idx'),
|
|
||||||
),
|
|
||||||
migrations.AddIndex(
|
|
||||||
model_name='notification',
|
|
||||||
index=models.Index(fields=['type', 'is_read'], name='user_manage_type_362052_idx'),
|
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
@ -0,0 +1,28 @@
|
|||||||
|
# Generated by Django 5.1.5 on 2025-03-28 07:41
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('user_management', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='feishucreator',
|
||||||
|
name='gmv',
|
||||||
|
field=models.CharField(blank=True, max_length=100, verbose_name='GMV'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='feishucreator',
|
||||||
|
name='price_quote',
|
||||||
|
field=models.TextField(blank=True, verbose_name='报价'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='feishucreator',
|
||||||
|
name='tiktok_url',
|
||||||
|
field=models.TextField(blank=True, verbose_name='链接'),
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.1.5 on 2025-03-28 07:45
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('user_management', '0002_alter_feishucreator_gmv_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='feishucreator',
|
||||||
|
name='handle',
|
||||||
|
field=models.TextField(blank=True, verbose_name='Handle'),
|
||||||
|
),
|
||||||
|
]
|
35
user_management/migrations/0004_knowledgebasedocument.py
Normal file
35
user_management/migrations/0004_knowledgebasedocument.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
# Generated by Django 5.1.5 on 2025-03-31 14:03
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import uuid
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('user_management', '0003_alter_feishucreator_handle'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='KnowledgeBaseDocument',
|
||||||
|
fields=[
|
||||||
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||||
|
('document_id', models.CharField(max_length=100, verbose_name='文档ID')),
|
||||||
|
('document_name', models.CharField(max_length=255, verbose_name='文档名称')),
|
||||||
|
('external_id', models.CharField(max_length=100, verbose_name='外部文档ID')),
|
||||||
|
('status', models.CharField(choices=[('active', '有效'), ('deleted', '已删除')], default='active', max_length=20, verbose_name='状态')),
|
||||||
|
('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||||
|
('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||||
|
('knowledge_base', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='kb_documents', to='user_management.knowledgebase', verbose_name='知识库')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': '知识库文档',
|
||||||
|
'verbose_name_plural': '知识库文档',
|
||||||
|
'db_table': 'knowledge_base_documents',
|
||||||
|
'indexes': [models.Index(fields=['knowledge_base', 'status'], name='knowledge_b_knowled_a4db1b_idx'), models.Index(fields=['document_id'], name='knowledge_b_documen_dab90f_idx'), models.Index(fields=['external_id'], name='knowledge_b_externa_b0060c_idx')],
|
||||||
|
'unique_together': {('knowledge_base', 'document_id')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,71 @@
|
|||||||
|
# Generated by Django 5.1.5 on 2025-04-09 15:16
|
||||||
|
|
||||||
|
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', '0004_knowledgebasedocument'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='GmailAttachment',
|
||||||
|
fields=[
|
||||||
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||||
|
('gmail_message_id', models.CharField(max_length=100, verbose_name='Gmail消息ID')),
|
||||||
|
('filename', models.CharField(max_length=255, verbose_name='文件名')),
|
||||||
|
('filepath', models.CharField(max_length=500, verbose_name='文件路径')),
|
||||||
|
('mimetype', models.CharField(max_length=100, verbose_name='MIME类型')),
|
||||||
|
('filesize', models.IntegerField(default=0, verbose_name='文件大小')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||||
|
('chat_message', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='gmail_attachments', to='user_management.chathistory')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Gmail附件',
|
||||||
|
'verbose_name_plural': 'Gmail附件',
|
||||||
|
'db_table': 'gmail_attachments',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='GmailCredential',
|
||||||
|
fields=[
|
||||||
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||||
|
('token_path', models.CharField(max_length=255, verbose_name='Token存储路径')),
|
||||||
|
('last_history_id', models.CharField(blank=True, max_length=100, null=True, verbose_name='上次同步历史ID')),
|
||||||
|
('watch_expiration', models.DateTimeField(blank=True, null=True, 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='gmail_credentials', to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Gmail认证凭据',
|
||||||
|
'verbose_name_plural': 'Gmail认证凭据',
|
||||||
|
'db_table': 'gmail_credentials',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='GmailTalentMapping',
|
||||||
|
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')),
|
||||||
|
('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='更新时间')),
|
||||||
|
('knowledge_base', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='gmail_mappings', to='user_management.knowledgebase')),
|
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='gmail_talent_mappings', to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Gmail达人映射',
|
||||||
|
'verbose_name_plural': 'Gmail达人映射',
|
||||||
|
'db_table': 'gmail_talent_mappings',
|
||||||
|
'unique_together': {('user', 'talent_email')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.1.5 on 2025-04-09 16:45
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('user_management', '0005_gmailattachment_gmailcredential_gmailtalentmapping'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='gmailcredential',
|
||||||
|
name='credentials',
|
||||||
|
field=models.BinaryField(blank=True, null=True, verbose_name='序列化的凭证数据'),
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,57 @@
|
|||||||
|
# Generated by Django 5.1.5 on 2025-04-10 09:17
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('user_management', '0006_gmailcredential_credentials'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='gmailcredential',
|
||||||
|
options={},
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='gmailcredential',
|
||||||
|
name='created_at',
|
||||||
|
field=models.DateTimeField(auto_now_add=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='gmailcredential',
|
||||||
|
name='credentials',
|
||||||
|
field=models.BinaryField(default=b''),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='gmailcredential',
|
||||||
|
name='is_active',
|
||||||
|
field=models.BooleanField(default=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='gmailcredential',
|
||||||
|
name='last_history_id',
|
||||||
|
field=models.CharField(blank=True, max_length=255, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='gmailcredential',
|
||||||
|
name='token_path',
|
||||||
|
field=models.CharField(max_length=255),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='gmailcredential',
|
||||||
|
name='updated_at',
|
||||||
|
field=models.DateTimeField(auto_now=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='gmailcredential',
|
||||||
|
name='watch_expiration',
|
||||||
|
field=models.DateTimeField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterModelTable(
|
||||||
|
name='gmailcredential',
|
||||||
|
table='gmail_credential',
|
||||||
|
),
|
||||||
|
]
|
@ -1,3 +1,4 @@
|
|||||||
|
from itertools import count
|
||||||
from django.contrib.auth.models import AbstractUser
|
from django.contrib.auth.models import AbstractUser
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
@ -286,13 +287,23 @@ class ChatHistory(models.Model):
|
|||||||
]
|
]
|
||||||
|
|
||||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||||
|
# 保留与主知识库的关联
|
||||||
knowledge_base = models.ForeignKey('KnowledgeBase', on_delete=models.CASCADE)
|
knowledge_base = models.ForeignKey('KnowledgeBase', on_delete=models.CASCADE)
|
||||||
|
# 用于标识知识库组合的对话
|
||||||
conversation_id = models.CharField(max_length=100, db_index=True)
|
conversation_id = models.CharField(max_length=100, db_index=True)
|
||||||
parent_id = models.CharField(max_length=100, null=True, blank=True)
|
parent_id = models.CharField(max_length=100, null=True, blank=True)
|
||||||
role = models.CharField(max_length=20, choices=ROLE_CHOICES)
|
role = models.CharField(max_length=20, choices=ROLE_CHOICES)
|
||||||
content = models.TextField()
|
content = models.TextField()
|
||||||
tokens = models.IntegerField(default=0, help_text="消息token数")
|
tokens = models.IntegerField(default=0, help_text="消息token数")
|
||||||
metadata = models.JSONField(default=dict, blank=True)
|
# 扩展metadata字段,用于存储知识库组合信息
|
||||||
|
metadata = models.JSONField(default=dict, blank=True, help_text="""
|
||||||
|
{
|
||||||
|
'model_id': 'xxx',
|
||||||
|
'dataset_id_list': ['id1', 'id2', ...],
|
||||||
|
'dataset_external_id_list': ['ext1', 'ext2', ...],
|
||||||
|
'primary_knowledge_base': 'id1'
|
||||||
|
}
|
||||||
|
""")
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
is_deleted = models.BooleanField(default=False)
|
is_deleted = models.BooleanField(default=False)
|
||||||
|
|
||||||
@ -302,6 +313,8 @@ class ChatHistory(models.Model):
|
|||||||
indexes = [
|
indexes = [
|
||||||
models.Index(fields=['conversation_id', 'created_at']),
|
models.Index(fields=['conversation_id', 'created_at']),
|
||||||
models.Index(fields=['user', 'created_at']),
|
models.Index(fields=['user', 'created_at']),
|
||||||
|
# 添加新的索引以支持知识库组合查询
|
||||||
|
models.Index(fields=['conversation_id', 'is_deleted']),
|
||||||
]
|
]
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
@ -315,11 +328,69 @@ class ChatHistory(models.Model):
|
|||||||
is_deleted=False
|
is_deleted=False
|
||||||
).order_by('created_at')
|
).order_by('created_at')
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_conversations_by_knowledge_bases(cls, dataset_ids, user):
|
||||||
|
"""根据知识库组合获取对话历史"""
|
||||||
|
# 对知识库ID列表排序以确保一致性
|
||||||
|
sorted_kb_ids = sorted(dataset_ids)
|
||||||
|
conversation_id = str(uuid.uuid5(
|
||||||
|
uuid.NAMESPACE_DNS,
|
||||||
|
'-'.join(sorted_kb_ids)
|
||||||
|
))
|
||||||
|
|
||||||
|
return cls.objects.filter(
|
||||||
|
conversation_id=conversation_id,
|
||||||
|
user=user,
|
||||||
|
is_deleted=False
|
||||||
|
).order_by('created_at')
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_knowledge_base_combinations(cls, user):
|
||||||
|
"""获取用户的所有知识库组合"""
|
||||||
|
return cls.objects.filter(
|
||||||
|
user=user,
|
||||||
|
is_deleted=False
|
||||||
|
).values('conversation_id').annotate(
|
||||||
|
last_message=max('created_at'),
|
||||||
|
message_count=count('id')
|
||||||
|
).values(
|
||||||
|
'conversation_id',
|
||||||
|
'last_message',
|
||||||
|
'message_count',
|
||||||
|
'metadata'
|
||||||
|
).order_by('-last_message')
|
||||||
|
|
||||||
|
def get_knowledge_bases(self):
|
||||||
|
"""获取此消息关联的所有知识库"""
|
||||||
|
if self.metadata and 'dataset_id_list' in self.metadata:
|
||||||
|
return KnowledgeBase.objects.filter(
|
||||||
|
id__in=self.metadata['dataset_id_list']
|
||||||
|
)
|
||||||
|
return KnowledgeBase.objects.filter(id=self.knowledge_base.id)
|
||||||
|
|
||||||
def soft_delete(self):
|
def soft_delete(self):
|
||||||
"""软删除消息"""
|
"""软删除消息"""
|
||||||
self.is_deleted = True
|
self.is_deleted = True
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
"""转换为字典格式"""
|
||||||
|
return {
|
||||||
|
'id': str(self.id),
|
||||||
|
'conversation_id': self.conversation_id,
|
||||||
|
'role': self.role,
|
||||||
|
'content': self.content,
|
||||||
|
'created_at': self.created_at.strftime('%Y-%m-%d %H:%M:%S'),
|
||||||
|
'metadata': self.metadata,
|
||||||
|
'knowledge_bases': [
|
||||||
|
{
|
||||||
|
'id': str(kb.id),
|
||||||
|
'name': kb.name,
|
||||||
|
'type': kb.type
|
||||||
|
} for kb in self.get_knowledge_bases()
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
class UserProfile(models.Model):
|
class UserProfile(models.Model):
|
||||||
"""用户档案模型"""
|
"""用户档案模型"""
|
||||||
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile')
|
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile')
|
||||||
@ -547,3 +618,150 @@ class KnowledgeBase(models.Model):
|
|||||||
embedding_mode_id=embedding_mode_id,
|
embedding_mode_id=embedding_mode_id,
|
||||||
document_count=len(data["documents"]) if data.get("documents") else 0
|
document_count=len(data["documents"]) if data.get("documents") else 0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
class FeishuCreator(models.Model):
|
||||||
|
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||||
|
record_id = models.CharField(max_length=100, unique=True, verbose_name='飞书记录ID')
|
||||||
|
|
||||||
|
# 对接人信息
|
||||||
|
contact_person = models.CharField(max_length=50, blank=True, verbose_name='对接人')
|
||||||
|
|
||||||
|
# 基本账号信息
|
||||||
|
handle = models.TextField(blank=True, verbose_name='Handle')
|
||||||
|
tiktok_url = models.TextField(blank=True, verbose_name='链接')
|
||||||
|
fans_count = models.CharField(max_length=50, blank=True, verbose_name='粉丝数')
|
||||||
|
gmv = models.CharField(max_length=100, blank=True, verbose_name='GMV')
|
||||||
|
|
||||||
|
# 联系方式
|
||||||
|
email = models.EmailField(blank=True, verbose_name='邮箱')
|
||||||
|
phone = models.CharField(max_length=50, blank=True, verbose_name='手机号|WhatsApp')
|
||||||
|
|
||||||
|
# 账号属性和报价
|
||||||
|
account_type = models.CharField(max_length=50, blank=True, verbose_name='账号属性')
|
||||||
|
price_quote = models.TextField(blank=True, verbose_name='报价')
|
||||||
|
response_speed = models.CharField(max_length=50, blank=True, verbose_name='回复速度')
|
||||||
|
|
||||||
|
# 合作相关
|
||||||
|
cooperation_intention = models.CharField(max_length=50, blank=True, verbose_name='合作意向')
|
||||||
|
payment_method = models.CharField(max_length=50, blank=True, verbose_name='支付方式')
|
||||||
|
payment_account = models.CharField(max_length=100, blank=True, verbose_name='收款账号')
|
||||||
|
address = models.TextField(blank=True, verbose_name='收件地址')
|
||||||
|
has_ooin = models.CharField(max_length=10, blank=True, verbose_name='签约OOIN?')
|
||||||
|
|
||||||
|
# 渠道和进度
|
||||||
|
source = models.CharField(max_length=100, blank=True, verbose_name='渠道来源')
|
||||||
|
contact_status = models.CharField(max_length=50, blank=True, verbose_name='建联进度')
|
||||||
|
cooperation_brands = models.JSONField(default=list, blank=True, verbose_name='合作品牌')
|
||||||
|
|
||||||
|
# 品类信息
|
||||||
|
system_categories = models.CharField(max_length=100, blank=True, verbose_name='系统展示的带货品类')
|
||||||
|
actual_categories = models.CharField(max_length=100, blank=True, verbose_name='实际高播放量带货品类')
|
||||||
|
human_categories = models.CharField(max_length=100, blank=True, verbose_name='达人标想要货品类')
|
||||||
|
|
||||||
|
# 其他信息
|
||||||
|
creator_base = models.CharField(max_length=100, blank=True, verbose_name='达人base')
|
||||||
|
notes = models.TextField(blank=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 = 'feishu_creators'
|
||||||
|
verbose_name = '创作者数据'
|
||||||
|
verbose_name_plural = '创作者数据'
|
||||||
|
|
||||||
|
class KnowledgeBaseDocument(models.Model):
|
||||||
|
"""知识库文档关联模型"""
|
||||||
|
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||||
|
knowledge_base = models.ForeignKey(
|
||||||
|
'KnowledgeBase',
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='kb_documents',
|
||||||
|
verbose_name='知识库'
|
||||||
|
)
|
||||||
|
document_id = models.CharField(max_length=100, verbose_name='文档ID')
|
||||||
|
document_name = models.CharField(max_length=255, verbose_name='文档名称')
|
||||||
|
external_id = models.CharField(max_length=100, verbose_name='外部文档ID')
|
||||||
|
status = models.CharField(
|
||||||
|
max_length=20,
|
||||||
|
default='active',
|
||||||
|
choices=[
|
||||||
|
('active', '有效'),
|
||||||
|
('deleted', '已删除')
|
||||||
|
],
|
||||||
|
verbose_name='状态'
|
||||||
|
)
|
||||||
|
create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
|
||||||
|
update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
db_table = 'knowledge_base_documents'
|
||||||
|
unique_together = ['knowledge_base', 'document_id']
|
||||||
|
indexes = [
|
||||||
|
models.Index(fields=['knowledge_base', 'status']),
|
||||||
|
models.Index(fields=['document_id']),
|
||||||
|
models.Index(fields=['external_id'])
|
||||||
|
]
|
||||||
|
verbose_name = '知识库文档'
|
||||||
|
verbose_name_plural = '知识库文档'
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.knowledge_base.name} - {self.document_name}"
|
||||||
|
|
||||||
|
class GmailCredential(models.Model):
|
||||||
|
"""Gmail认证信息"""
|
||||||
|
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||||
|
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='gmail_credentials')
|
||||||
|
gmail_email = models.EmailField(max_length=255, null=True, blank=True, help_text="实际授权的Gmail账号,可能与user.email不同")
|
||||||
|
credentials = models.BinaryField() # 序列化的凭证对象
|
||||||
|
token_path = models.CharField(max_length=255) # token存储路径
|
||||||
|
last_history_id = models.CharField(max_length=255, null=True, blank=True) # 最后处理的historyId
|
||||||
|
watch_expiration = models.DateTimeField(null=True, blank=True) # 监听过期时间
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
is_active = models.BooleanField(default=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
db_table = 'gmail_credential'
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.user.username}的Gmail认证"
|
||||||
|
|
||||||
|
class GmailTalentMapping(models.Model):
|
||||||
|
"""Gmail达人映射关系模型"""
|
||||||
|
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||||
|
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='gmail_talent_mappings')
|
||||||
|
talent_email = models.EmailField(verbose_name='达人邮箱')
|
||||||
|
knowledge_base = models.ForeignKey(KnowledgeBase, on_delete=models.CASCADE, related_name='gmail_mappings')
|
||||||
|
conversation_id = models.CharField(max_length=100, verbose_name='对话ID')
|
||||||
|
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 = 'gmail_talent_mappings'
|
||||||
|
unique_together = ['user', 'talent_email']
|
||||||
|
verbose_name = 'Gmail达人映射'
|
||||||
|
verbose_name_plural = 'Gmail达人映射'
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.user.username} - {self.talent_email}"
|
||||||
|
|
||||||
|
class GmailAttachment(models.Model):
|
||||||
|
"""Gmail附件模型"""
|
||||||
|
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||||
|
chat_message = models.ForeignKey(ChatHistory, on_delete=models.CASCADE, related_name='gmail_attachments')
|
||||||
|
gmail_message_id = models.CharField(max_length=100, verbose_name='Gmail消息ID')
|
||||||
|
filename = models.CharField(max_length=255, verbose_name='文件名')
|
||||||
|
filepath = models.CharField(max_length=500, verbose_name='文件路径')
|
||||||
|
mimetype = models.CharField(max_length=100, verbose_name='MIME类型')
|
||||||
|
filesize = models.IntegerField(default=0, verbose_name='文件大小')
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
db_table = 'gmail_attachments'
|
||||||
|
verbose_name = 'Gmail附件'
|
||||||
|
verbose_name_plural = 'Gmail附件'
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.filename} ({self.gmail_message_id})"
|
||||||
|
@ -1,45 +1,69 @@
|
|||||||
from django.urls import path, include
|
from django.urls import path, include
|
||||||
from rest_framework.routers import DefaultRouter
|
from rest_framework.routers import DefaultRouter
|
||||||
from .views import (
|
from .views import (
|
||||||
KnowledgeBaseViewSet,
|
KnowledgeBaseViewSet,
|
||||||
PermissionViewSet,
|
PermissionViewSet,
|
||||||
NotificationViewSet,
|
NotificationViewSet,
|
||||||
verify_token,
|
verify_token,
|
||||||
user_list,
|
user_list,
|
||||||
user_detail,
|
user_detail,
|
||||||
user_update,
|
user_update,
|
||||||
user_delete,
|
user_delete,
|
||||||
change_password,
|
change_password,
|
||||||
RegisterView,
|
RegisterView,
|
||||||
LoginView,
|
LoginView,
|
||||||
LogoutView,
|
LogoutView,
|
||||||
ChatHistoryViewSet
|
ChatHistoryViewSet,
|
||||||
)
|
user_profile,
|
||||||
|
user_register,
|
||||||
# 创建路由器
|
setup_gmail_integration,
|
||||||
router = DefaultRouter()
|
send_gmail_message,
|
||||||
|
gmail_webhook,
|
||||||
# 注册视图集
|
get_gmail_attachments,
|
||||||
router.register(r'knowledge-bases', KnowledgeBaseViewSet, basename='knowledge-base')
|
download_gmail_attachment,
|
||||||
router.register(r'permissions', PermissionViewSet, basename='permission')
|
get_gmail_talents,
|
||||||
router.register(r'notifications', NotificationViewSet, basename='notification')
|
refresh_gmail_watch,
|
||||||
router.register(r'chat-history', ChatHistoryViewSet, basename='chat-history')
|
check_gmail_auth,
|
||||||
|
import_gmail_from_sender,
|
||||||
# URL patterns
|
sync_talent_emails
|
||||||
urlpatterns = [
|
)
|
||||||
# API 路由
|
|
||||||
path('', include(router.urls)),
|
# 创建路由器
|
||||||
|
router = DefaultRouter()
|
||||||
# 用户认证相关
|
|
||||||
path('auth/register/', RegisterView.as_view(), name='register'),
|
# 注册视图集
|
||||||
path('auth/login/', LoginView.as_view(), name='login'),
|
router.register(r'knowledge-bases', KnowledgeBaseViewSet, basename='knowledge-bases')
|
||||||
path('auth/logout/', LogoutView.as_view(), name='logout'),
|
router.register(r'permissions', PermissionViewSet, basename='permission')
|
||||||
path('auth/verify-token/', verify_token, name='verify-token'),
|
router.register(r'notifications', NotificationViewSet, basename='notification')
|
||||||
path('auth/change-password/', change_password, name='change-password'),
|
router.register(r'chat-history', ChatHistoryViewSet, basename='chat-history')
|
||||||
|
|
||||||
# 用户管理相关
|
# URL patterns
|
||||||
path('users/', user_list, name='user-list'),
|
urlpatterns = [
|
||||||
path('users/<str:pk>/', user_detail, name='user-detail'),
|
# API 路由
|
||||||
path('users/<str:pk>/update/', user_update, name='user-update'),
|
path('', include(router.urls)),
|
||||||
path('users/<str:pk>/delete/', user_delete, name='user-delete'),
|
|
||||||
]
|
# 用户认证相关
|
||||||
|
path('auth/register/', RegisterView.as_view(), name='register'),
|
||||||
|
path('auth/login/', LoginView.as_view(), name='login'),
|
||||||
|
path('auth/logout/', LogoutView.as_view(), name='logout'),
|
||||||
|
path('auth/verify-token/', verify_token, name='verify-token'),
|
||||||
|
path('auth/change-password/', change_password, name='change-password'),
|
||||||
|
|
||||||
|
# 用户管理相关
|
||||||
|
path('users/', user_list, name='user-list'),
|
||||||
|
path('users/<str:pk>/', user_detail, name='user-detail'),
|
||||||
|
path('users/<str:pk>/update/', user_update, name='user-update'),
|
||||||
|
path('users/<str:pk>/delete/', user_delete, name='user-delete'),
|
||||||
|
|
||||||
|
# Gmail集成API
|
||||||
|
path('gmail/setup/', setup_gmail_integration, name='setup_gmail_integration'),
|
||||||
|
path('gmail/send/', send_gmail_message, name='send_gmail_message'),
|
||||||
|
path('gmail/webhook/', gmail_webhook, name='gmail_webhook'),
|
||||||
|
path('gmail/attachments/', get_gmail_attachments, name='get_gmail_attachments'),
|
||||||
|
path('gmail/download/', download_gmail_attachment, name='download_gmail_attachment'),
|
||||||
|
path('gmail/talents/', get_gmail_talents, name='get_gmail_talents'),
|
||||||
|
path('gmail/refresh-watch/', refresh_gmail_watch, name='refresh_gmail_watch'),
|
||||||
|
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'),
|
||||||
|
]
|
||||||
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user