Compare commits

...

10 Commits

Author SHA1 Message Date
wanjia
45d49d4b4a 初始提交 - Gmail集成功能优化 2025-04-10 18:25:59 +08:00
wanjia
083971a94a gmail添加附件 2025-04-07 15:07:20 +08:00
wanjia
b64af631e3 上传文档相关功能完善,删除会话功能增加 2025-04-07 12:41:47 +08:00
wanjia
5eb4ccd7fd 文档相关功能 2025-04-02 12:25:40 +08:00
wanjia
a90d31c420 完善权限 2025-03-29 12:26:50 +08:00
wanjia
979967ed7d 权限列表正确显示 2025-03-26 12:26:20 +08:00
wanjia
9153594375 conversation_id显示用户对话 2025-03-21 15:10:29 +08:00
wanjia
c0bba14ee8 调用api进行对话 2025-03-20 13:48:36 +08:00
wanjia
00deeb711b 管理员直接修改用户权限 2025-03-17 16:46:37 +08:00
wanjia
dfef072a8c 创建者修改知识库类型 2025-03-13 13:39:02 +08:00
32 changed files with 9915 additions and 2899 deletions

BIN
data_backup.json Normal file

Binary file not shown.

View File

@ -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
View File

290
feishu/feishu.py Normal file
View 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()

View 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
View 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"]}}

View 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": []
}
]

View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1 @@
你好

View File

@ -0,0 +1,10 @@
本期节目内容简介
在参加各类比赛时,肯定会遇到竞争对手。英语单词 rival、opponent、competitor 和 contestant 的含义相似,都可以用来指“与他人之间存在竞争关系的人或团队”。在本集《你问我答》节目中,我们将通过和体育比赛有关的实例来为大家阐释这四个近义词之间的区别和用法。
欢迎你加入并和我们一起讨论英语学习的方方面面。请通过微博“BBC英语教学”或邮件与我们取得联系。我们的邮箱地址是 questions.chinaelt@bbc.co.uk。
文字稿
(关于台词的备注: 请注意这不是广播节目的逐字稿件。本文稿可能没有体现录制、编辑过程中对节目做出的改变。)
Feifei
大家好,欢迎收听 BBC 英语教学的《你问我答》节目,我是冯菲菲。每集节目中,我们会回答大家在英语学习时遇到的一个问题。本集的问题来自 Adela。我们来听一下她的问题。

View 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"}

View File

@ -104,8 +104,16 @@ DATABASES = {
'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 字符集
'init_command': "SET sql_mode='STRICT_TRANS_TABLES'; SET innodb_strict_mode=1; SET NAMES utf8mb4;",
},
'TEST': {
'CHARSET': 'utf8mb4',
'COLLATION': 'utf8mb4_unicode_ci',
},
} }
} }
# Password validation # Password validation
@ -130,11 +138,11 @@ AUTH_PASSWORD_VALIDATORS = [
LANGUAGE_CODE = 'en-us' LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC' TIME_ZONE = 'Asia/Shanghai'
USE_I18N = True USE_I18N = True
USE_TZ = True USE_TZ = True # 将此项设置为True以启用时区支持
@ -286,3 +294,26 @@ REST_FRAMEWORK = {
'rest_framework.parsers.MultiPartParser' '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以启用时区支持

View File

@ -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
View 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"]}}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1 @@
# 管理命令包

View File

@ -0,0 +1 @@
# Gmail管理命令

View File

@ -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)}"))

View File

@ -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'),
), ),
] ]

View File

@ -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='链接'),
),
]

View File

@ -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'),
),
]

View 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')},
},
),
]

View File

@ -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')},
},
),
]

View File

@ -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='序列化的凭证数据'),
),
]

View File

@ -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',
),
]

View File

@ -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})"

View File

@ -13,14 +13,26 @@ from .views import (
RegisterView, RegisterView,
LoginView, LoginView,
LogoutView, LogoutView,
ChatHistoryViewSet ChatHistoryViewSet,
user_profile,
user_register,
setup_gmail_integration,
send_gmail_message,
gmail_webhook,
get_gmail_attachments,
download_gmail_attachment,
get_gmail_talents,
refresh_gmail_watch,
check_gmail_auth,
import_gmail_from_sender,
sync_talent_emails
) )
# 创建路由器 # 创建路由器
router = DefaultRouter() router = DefaultRouter()
# 注册视图集 # 注册视图集
router.register(r'knowledge-bases', KnowledgeBaseViewSet, basename='knowledge-base') router.register(r'knowledge-bases', KnowledgeBaseViewSet, basename='knowledge-bases')
router.register(r'permissions', PermissionViewSet, basename='permission') router.register(r'permissions', PermissionViewSet, basename='permission')
router.register(r'notifications', NotificationViewSet, basename='notification') router.register(r'notifications', NotificationViewSet, basename='notification')
router.register(r'chat-history', ChatHistoryViewSet, basename='chat-history') router.register(r'chat-history', ChatHistoryViewSet, basename='chat-history')
@ -42,4 +54,16 @@ urlpatterns = [
path('users/<str:pk>/', user_detail, name='user-detail'), path('users/<str:pk>/', user_detail, name='user-detail'),
path('users/<str:pk>/update/', user_update, name='user-update'), path('users/<str:pk>/update/', user_update, name='user-update'),
path('users/<str:pk>/delete/', user_delete, name='user-delete'), 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