diff --git a/data_backup.json b/data_backup.json new file mode 100644 index 00000000..368f99f2 Binary files /dev/null and b/data_backup.json differ diff --git a/feishu/__init__.py b/feishu/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/feishu/feishu.py b/feishu/feishu.py new file mode 100644 index 00000000..7063d35d --- /dev/null +++ b/feishu/feishu.py @@ -0,0 +1,184 @@ +import json +import django +import os +import sys + +# 设置 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 main(): + # 创建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: # 每10条才打印一次,避免输出过多 + 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)}") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/gmail/client_secret.json b/gmail/client_secret.json new file mode 100644 index 00000000..c9852c2b --- /dev/null +++ b/gmail/client_secret.json @@ -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"]}} \ No newline at end of file diff --git a/gmail/email_conversations.json b/gmail/email_conversations.json new file mode 100644 index 00000000..2756391a --- /dev/null +++ b/gmail/email_conversations.json @@ -0,0 +1,20 @@ +[ + { + "subject": "这是主题", + "from": "crush wds ", + "date": "2025-03-27 12:04:29", + "body": "你好呀\r\n" + }, + { + "subject": "", + "from": "wds crush ", + "date": "2025-03-27 12:05:54", + "body": "你那里天气怎么样\r\n" + }, + { + "subject": "", + "from": "wds crush ", + "date": "2025-03-27 13:13:03", + "body": "吃饭了吗\r\n" + } +] \ No newline at end of file diff --git a/gmail/email_conversations.txt b/gmail/email_conversations.txt new file mode 100644 index 00000000..578ce5be --- /dev/null +++ b/gmail/email_conversations.txt @@ -0,0 +1,25 @@ +================================================== +记录时间: 2025-03-27 13:15:18 +================================================== + +时间: 2025-03-27 12:04:29 +发件人: crush wds +主题: 这是主题 +内容: +你好呀 + +-------------------------------------------------- +时间: 2025-03-27 12:05:54 +发件人: wds crush +主题: +内容: +你那里天气怎么样 + +-------------------------------------------------- +时间: 2025-03-27 13:13:03 +发件人: wds crush +主题: +内容: +吃饭了吗 + +-------------------------------------------------- diff --git a/gmail/gmail_read.py b/gmail/gmail_read.py new file mode 100644 index 00000000..c568f7f2 --- /dev/null +++ b/gmail/gmail_read.py @@ -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" ', + '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) diff --git a/gmail/quickstart.py b/gmail/quickstart.py new file mode 100644 index 00000000..dd2c5e07 --- /dev/null +++ b/gmail/quickstart.py @@ -0,0 +1,171 @@ +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://www.googleapis.com/auth/gmail.readonly' +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 get_email_content(message): + """提取邮件内容""" + try: + payload = message['payload'] + headers = payload['headers'] + + # 获取邮件基本信息 + email_data = { + 'subject': '', + 'from': '', + 'date': '', + 'body': '' + } + + # 提取头部信息 + 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') + + # 提取邮件正文 + if 'parts' in payload: + parts = payload['parts'] + for part in parts: + if part['mimeType'] == 'text/plain': + data = part['body'].get('data', '') + if data: + text = base64.urlsafe_b64decode(data).decode('utf-8') + email_data['body'] = text + break + 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") + 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)} 条对话记录") + + # 保存对话记录(追加模式) + save_conversations(conversations, output_file) + + # 打印对话统计 + print("\n对话统计:") + print(f"总消息数: {len(conversations)}") + 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} 条消息") + else: + print("未找到对话记录") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/gmail/requirements.txt b/gmail/requirements.txt new file mode 100644 index 00000000..34aaede8 --- /dev/null +++ b/gmail/requirements.txt @@ -0,0 +1,3 @@ +google-api-python-client==1.7.8 +google-auth-httplib2==0.0.3 +google-auth-oauthlib==0.4.0 \ No newline at end of file diff --git a/gmail/storage.json b/gmail/storage.json new file mode 100644 index 00000000..a53fb2fe --- /dev/null +++ b/gmail/storage.json @@ -0,0 +1 @@ +{"access_token": "ya29.a0AeXRPp7jfizXj6NfUavzABcDOEH-PD7TckkeCwQWFksLZwkFXqAaVe1gCeZZIWtP4hMD3F2-K7-hdSQAzCiyP7Z_72YPr_r3UDfflr6XOV7czJ937QknW_236kLJK1DDggpB4BFCB1cXtSJa6V4pbyez4lkka021mZWegOdGaCgYKATkSARISFQHGX2Mi7Ua3f90G-JLtvzu2YJgvoA0175", "client_id": "240872828479-570luspoc31259l1faab6kmjmpcsa9n9.apps.googleusercontent.com", "client_secret": "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", "refresh_token": "1//0eUNnePCc4hEKCgYIARAAGA4SNwF-L9IrIRfxg1JwnMu0WHZAu-uCWT7cx1PncId3bPZr4x53Mo57xGXr2WjiF4szuSQQkeL4hNU", "token_expiry": "2025-03-27T06:13:12Z", "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.a0AeXRPp7jfizXj6NfUavzABcDOEH-PD7TckkeCwQWFksLZwkFXqAaVe1gCeZZIWtP4hMD3F2-K7-hdSQAzCiyP7Z_72YPr_r3UDfflr6XOV7czJ937QknW_236kLJK1DDggpB4BFCB1cXtSJa6V4pbyez4lkka021mZWegOdGaCgYKATkSARISFQHGX2Mi7Ua3f90G-JLtvzu2YJgvoA0175", "expires_in": 3599, "scope": "https://www.googleapis.com/auth/gmail.modify", "token_type": "Bearer", "refresh_token_expires_in": 599654}, "scopes": ["https://www.googleapis.com/auth/gmail.modify"], "token_info_uri": "https://oauth2.googleapis.com/tokeninfo", "invalid": false, "_class": "OAuth2Credentials", "_module": "oauth2client.client"} \ No newline at end of file diff --git a/gmail/test.txt b/gmail/test.txt new file mode 100644 index 00000000..e6076a05 --- /dev/null +++ b/gmail/test.txt @@ -0,0 +1 @@ +你好 diff --git a/gmail/token.json b/gmail/token.json new file mode 100644 index 00000000..c608f0f2 --- /dev/null +++ b/gmail/token.json @@ -0,0 +1 @@ +{"token": "ya29.a0AeXRPp5MvarP9U4JZ3ZlV_xpIeHNL7IK1HEQLX6qhY-thS5IOFsdHrcNwbdNqkYrClFm4yGiMCXzWO-IilVbdSrDfEDw-Qfs7V9oTsZS2RYjbgcSR9rLUtZlL8ObnaljQie5VVzQZysHPZcMSdWiqabcQq2UsU5TNuOWo4J7_waCgYKAQESARISFQHGX2MiAR1O_HNYd0F6z0Rj-vSybA0177", "refresh_token": "1//0e1Usc30XxlXRCgYIARAAGA4SNwF-L9IrOOjEN7EkOSatDMHzTy0KaNdp_jf1XtLn7a5pFSljo3IKnrDdvVnxTO_FX5Asj_UDiAw", "token_uri": "https://oauth2.googleapis.com/token", "client_id": "240872828479-570luspoc31259l1faab6kmjmpcsa9n9.apps.googleusercontent.com", "client_secret": "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", "scopes": ["https://www.googleapis.com/auth/gmail.readonly"], "universe_domain": "googleapis.com", "account": "", "expiry": "2025-03-27T04:39:31.274041Z"} \ No newline at end of file diff --git a/role_based_system/settings.py b/role_based_system/settings.py index 9a12460e..9f61610c 100644 --- a/role_based_system/settings.py +++ b/role_based_system/settings.py @@ -106,7 +106,15 @@ DATABASES = { 'PASSWORD': '123456', 'HOST': 'localhost', '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 # https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators diff --git a/user_management/migrations/0001_initial.py b/user_management/migrations/0001_initial.py index cd1103fd..77d2eef6 100644 --- a/user_management/migrations/0001_initial.py +++ b/user_management/migrations/0001_initial.py @@ -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.validators @@ -18,6 +18,43 @@ class Migration(migrations.Migration): ] 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( name='User', fields=[ @@ -73,79 +110,22 @@ class Migration(migrations.Migration): name='KnowledgeBase', fields=[ ('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='知识库描述')), ('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)), ('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)), ('char_length', models.IntegerField(default=0)), ('document_count', models.IntegerField(default=0)), ('external_id', models.UUIDField(blank=True, null=True)), ('create_time', models.DateTimeField(auto_now_add=True)), ('update_time', models.DateTimeField(auto_now=True)), - ('owners', models.ManyToManyField(related_name='owned_knowledge_bases', to=settings.AUTH_USER_MODEL, verbose_name='所有者')), ], options={ 'db_table': 'knowledge_bases', - }, - ), - 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'], + '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( @@ -181,40 +161,65 @@ class Migration(migrations.Migration): 'db_table': 'user_profiles', }, ), - migrations.AddIndex( - model_name='knowledgebase', - index=models.Index(fields=['type'], name='knowledge_b_type_0439e7_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, 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( - model_name='knowledgebase', - index=models.Index(fields=['department'], name='knowledge_b_departm_e739fd_idx'), + 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', + 'indexes': [models.Index(fields=['knowledge_base', 'user', 'status'], name='knowledge_b_knowled_88e81e_idx')], + 'unique_together': {('knowledge_base', 'user')}, + }, ), - migrations.AddIndex( - model_name='knowledgebase', - index=models.Index(fields=['group'], name='knowledge_b_group_3dcf34_idx'), - ), - migrations.AddIndex( - model_name='chathistory', - index=models.Index(fields=['conversation_id', 'created_at'], name='chat_histor_convers_33721a_idx'), - ), - migrations.AddIndex( - model_name='chathistory', - index=models.Index(fields=['user', 'created_at'], name='chat_histor_user_id_aa050a_idx'), - ), - migrations.AddIndex( - model_name='knowledgebasepermission', - index=models.Index(fields=['knowledge_base', 'user', 'status'], name='knowledge_b_knowled_88e81e_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'), + 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'], + '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')], + }, ), ] diff --git a/user_management/migrations/0002_alter_feishucreator_gmv_and_more.py b/user_management/migrations/0002_alter_feishucreator_gmv_and_more.py new file mode 100644 index 00000000..6f073588 --- /dev/null +++ b/user_management/migrations/0002_alter_feishucreator_gmv_and_more.py @@ -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='链接'), + ), + ] diff --git a/user_management/migrations/0002_remove_knowledgebase_owners_alter_knowledgebase_name_and_more.py b/user_management/migrations/0002_remove_knowledgebase_owners_alter_knowledgebase_name_and_more.py deleted file mode 100644 index e5951885..00000000 --- a/user_management/migrations/0002_remove_knowledgebase_owners_alter_knowledgebase_name_and_more.py +++ /dev/null @@ -1,27 +0,0 @@ -# Generated by Django 5.1.5 on 2025-03-26 02:50 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('user_management', '0001_initial'), - ] - - operations = [ - migrations.RemoveField( - model_name='knowledgebase', - name='owners', - ), - migrations.AlterField( - model_name='knowledgebase', - name='name', - field=models.CharField(max_length=100, unique=True, verbose_name='知识库名称'), - ), - migrations.AlterField( - model_name='knowledgebase', - name='user_id', - field=models.UUIDField(verbose_name='创建者ID'), - ), - ] diff --git a/user_management/migrations/0003_alter_feishucreator_handle.py b/user_management/migrations/0003_alter_feishucreator_handle.py new file mode 100644 index 00000000..43ff9055 --- /dev/null +++ b/user_management/migrations/0003_alter_feishucreator_handle.py @@ -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'), + ), + ] diff --git a/user_management/models.py b/user_management/models.py index 91f35cab..1139e57d 100644 --- a/user_management/models.py +++ b/user_management/models.py @@ -1,3 +1,4 @@ +from itertools import count from django.contrib.auth.models import AbstractUser from django.db import models from django.utils import timezone @@ -286,13 +287,23 @@ class ChatHistory(models.Model): ] user = models.ForeignKey(User, on_delete=models.CASCADE) + # 保留与主知识库的关联 knowledge_base = models.ForeignKey('KnowledgeBase', on_delete=models.CASCADE) + # 用于标识知识库组合的对话 conversation_id = models.CharField(max_length=100, db_index=True) parent_id = models.CharField(max_length=100, null=True, blank=True) role = models.CharField(max_length=20, choices=ROLE_CHOICES) content = models.TextField() 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) is_deleted = models.BooleanField(default=False) @@ -302,6 +313,8 @@ class ChatHistory(models.Model): indexes = [ models.Index(fields=['conversation_id', 'created_at']), models.Index(fields=['user', 'created_at']), + # 添加新的索引以支持知识库组合查询 + models.Index(fields=['conversation_id', 'is_deleted']), ] def __str__(self): @@ -315,11 +328,69 @@ class ChatHistory(models.Model): is_deleted=False ).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): """软删除消息""" self.is_deleted = True 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): """用户档案模型""" user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile') @@ -547,3 +618,54 @@ class KnowledgeBase(models.Model): embedding_mode_id=embedding_mode_id, 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 = '创作者数据' diff --git a/user_management/views.py b/user_management/views.py index 40133cfe..f411efbb 100644 --- a/user_management/views.py +++ b/user_management/views.py @@ -1091,46 +1091,51 @@ class KnowledgeBaseViewSet(viewsets.ModelViewSet): def get_queryset(self): """获取用户有权限查看的知识库列表""" user = self.request.user - all_knowledge_bases = KnowledgeBase.objects.all() + queryset = KnowledgeBase.objects.all() - # 首先过滤出用户创建的或有显式读权限的知识库 - explicit_permission_kbs = KnowledgeBase.objects.filter( - Q(user_id=user.id) | # 是创建者 - Q( # 或在权限表中有读权限 - id__in=KBPermissionModel.objects.filter( - user=user, - can_read=True, - status='active' - ).values_list('knowledge_base_id', flat=True) - ) + # 1. 构建基础权限条件 + permission_conditions = Q() + + # 2. 所有用户都可以看到 admin 类型的知识库 + permission_conditions |= Q(type='admin') + + # 3. 用户可以看到自己创建的所有知识库 + permission_conditions |= Q(user_id=user.id) + + # 4. 添加显式权限条件 + permission_conditions |= Q( + id__in=KBPermissionModel.objects.filter( + user=user, + can_read=True, + status='active', + expires_at__gt=timezone.now() + ).values_list('knowledge_base_id', flat=True) ) - # 获取显式权限知识库的ID集合 - explicit_kb_ids = set(explicit_permission_kbs.values_list('id', flat=True)) - - # 对于没有显式权限的知识库,使用_can_read方法判断 - result_kbs = list(explicit_permission_kbs) - - # 如果用户是管理员,可以查看所有知识库 + # 5. 根据用户角色添加隐式权限 if user.role == 'admin': - return all_knowledge_bases + # 管理员可以看到除了其他用户 private 类型外的所有知识库 + permission_conditions |= ~Q(type='private') | Q(user_id=user.id) + elif user.role == 'leader': + # 组长可以查看本部门的 leader 和 member 类型知识库 + permission_conditions |= Q( + type__in=['leader', 'member'], + department=user.department + ) + elif user.role in ['member', 'user']: + # 成员可以查看本部门的 leader 类型知识库 + permission_conditions |= Q( + type='leader', + department=user.department + ) + # 成员可以查看本部门本组的 member 类型知识库 + permission_conditions |= Q( + type='member', + department=user.department, + group=user.group + ) - # 否则,根据角色和部门/组筛选 - for kb in all_knowledge_bases: - if kb.id not in explicit_kb_ids: - has_read_permission = self._can_read( - type=kb.type, - user=user, - department=kb.department, - group=kb.group, - creator_id=kb.user_id - ) - if has_read_permission: - result_kbs.append(kb) - - # 转换为queryset并去重 - kb_ids = [kb.id for kb in result_kbs] - return KnowledgeBase.objects.filter(id__in=kb_ids).distinct() + return queryset.filter(permission_conditions).distinct() def create(self, request, *args, **kwargs): try: @@ -1338,74 +1343,95 @@ class KnowledgeBaseViewSet(viewsets.ModelViewSet): def _can_edit(self, type, user, department=None, group=None, creator_id=None): """判断用户是否有编辑权限""" + # admin 类型知识库所有用户都有编辑权限 + if type == 'admin': + return True + if user.role == 'admin': - return True # 管理员对所有知识库都有编辑权限 - + # 管理员对其他用户的 private 类型没有编辑权限 + if type == 'private' and str(user.id) != str(creator_id): + return False + return True + if type == 'secret': - return False # 除管理员外,其他人无权编辑secret类型知识库 - + return False # 除管理员外,其他人无权编辑 secret 类型知识库 + if type == 'leader': - # 同部门组长可编辑leader知识库 + # 同部门组长可编辑 leader 知识库 return user.role == 'leader' and user.department == department - + if type == 'member': - # 同部门组长可编辑所有member知识库 + # 同部门组长可编辑所有 member 知识库 if user.role == 'leader' and user.department == department: return True - # 成员只能编辑同部门同组的member知识库 + # 成员只能编辑同部门同组的 member 知识库 return user.role == 'member' and user.department == department and user.group == group - + if type == 'private': # 私有知识库只有创建者可编辑 return str(user.id) == str(creator_id) - + return False def _can_delete(self, type, user, department=None, group=None, creator_id=None): """判断用户是否有删除权限""" + # admin 类型知识库所有用户都有删除权限 + if type == 'admin': + return True + if user.role == 'admin': - return True # 管理员对所有知识库都有删除权限 - - if type in ['admin', 'secret']: - return False # 除管理员外,其他人无权删除admin和secret类型知识库 - + # 管理员对其他用户的 private 类型没有删除权限 + if type == 'private' and str(user.id) != str(creator_id): + return False + return True + + if type == 'secret': + return False # 除管理员外,其他人无权删除 admin 和 secret 类型知识库 + if type == 'leader': - # 同部门组长可删除leader知识库 + # 同部门组长可删除 leader 知识库 return user.role == 'leader' and user.department == department - + if type == 'member': # 只有组长可以删除成员知识库(组员不能删除) return user.role == 'leader' and user.department == department - + if type == 'private': # 私有知识库只有创建者可删除 return str(user.id) == str(creator_id) - + return False def _can_read(self, type, user, department=None, group=None, creator_id=None): """判断用户是否有读取权限""" + # admin 类型知识库所有用户都有读取权限 + if type == 'admin': + return True + if user.role == 'admin': - return True # 管理员可以读取所有知识库 - + # 管理员对其他用户的 private 类型没有读取权限 + if type == 'private' and str(user.id) != str(creator_id): + return False + return True + if type == 'secret': - return False # 除管理员外,secret类型知识库对其他人不可读 - + return False # 除管理员外,secret 类型知识库对其他人不可读 + if type == 'leader': - # 同部门的leader和member都可以读取 + # 同部门的 leader 和 member 都可以读取 return user.department == department - + if type == 'member': - # 同部门组长可以读取所有member知识库 + # 同部门组长可以读取所有 member 知识库 if user.role == 'leader' and user.department == department: return True - # 成员可以读取同部门同组的member知识库 + # 成员可以读取同部门同组的 member 知识库 return user.role == 'member' and user.department == department and user.group == group - + if type == 'private': # 私有知识库只有创建者可读 return str(user.id) == str(creator_id) - + return False def _create_external_dataset(self, instance):