diff --git a/README_PLATFORM_ACCOUNT_EXTENDED.md b/README_PLATFORM_ACCOUNT_EXTENDED.md new file mode 100644 index 0000000..592efcf --- /dev/null +++ b/README_PLATFORM_ACCOUNT_EXTENDED.md @@ -0,0 +1,153 @@ +# 平台账号扩展功能使用指南 + +本文档介绍平台账号(PlatformAccount)模型的扩展字段和相关API的使用方法。 + +## 新增字段说明 + +平台账号模型新增了以下字段: + +| 字段名称 | 类型 | 说明 | +|---------|------|------| +| tags | 字符串 | 账号标签,以逗号分隔的标签列表 | +| profile_image | URL | 账号头像URL | +| last_posting | 日期时间 | 最后发布内容的时间 | + +## API接口使用 + +### 1. 创建平台账号 + +在创建平台账号时,可以同时设置标签、头像和最后发布时间。 + +**请求URL**: `POST /api/operation/platforms/` + +**请求参数**: +```json +{ + "operator": 1, + "platform_name": "youtube", + "account_name": "test_channel", + "account_id": "UC12345", + "status": "active", + "followers_count": 1000, + "account_url": "https://youtube.com/test_channel", + "description": "测试频道描述", + "tags": "科技,教育,AI", + "profile_image": "https://example.com/profile.jpg", + "last_posting": "2023-09-01T10:00:00Z" +} +``` + +**成功响应**: +```json +{ + "code": 200, + "message": "平台账号创建成功,并已添加到知识库", + "data": { + "id": 1, + "operator": 1, + "operator_name": "测试运营", + "platform_name": "youtube", + "account_name": "test_channel", + "account_id": "UC12345", + "status": "active", + "followers_count": 1000, + "account_url": "https://youtube.com/test_channel", + "description": "测试频道描述", + "tags": "科技,教育,AI", + "profile_image": "https://example.com/profile.jpg", + "last_posting": "2023-09-01T10:00:00Z", + "created_at": "2023-09-01T10:00:00Z", + "updated_at": "2023-09-01T10:00:00Z", + "last_login": null + } +} +``` + +### 2. 更新平台账号资料 + +除了使用标准的PUT/PATCH方法更新平台账号外,还提供了一个专门用于更新标签、头像和最后发布时间的接口。 + +**请求URL**: `POST /api/operation/platforms/{id}/update_profile/` + +**请求参数**: +```json +{ + "tags": "科技,编程,AI", + "profile_image": "https://example.com/new_profile.jpg", + "last_posting": "2023-09-10T15:30:00Z" +} +``` + +**成功响应**: +```json +{ + "code": 200, + "message": "平台账号资料更新成功", + "data": { + "id": 1, + "operator": 1, + "operator_name": "测试运营", + "platform_name": "youtube", + "account_name": "test_channel", + "account_id": "UC12345", + "status": "active", + "followers_count": 1000, + "account_url": "https://youtube.com/test_channel", + "description": "测试频道描述", + "tags": "科技,编程,AI", + "profile_image": "https://example.com/new_profile.jpg", + "last_posting": "2023-09-10T15:30:00Z", + "created_at": "2023-09-01T10:00:00Z", + "updated_at": "2023-09-10T15:35:00Z", + "last_login": null + } +} +``` + +### 3. 获取平台账号详情 + +获取平台账号详情时,也会返回标签、头像和最后发布时间信息。 + +**请求URL**: `GET /api/operation/platforms/{id}/` + +**成功响应**: +```json +{ + "code": 200, + "message": "获取平台账号详情成功", + "data": { + "id": 1, + "operator": 1, + "operator_name": "测试运营", + "platform_name": "youtube", + "account_name": "test_channel", + "account_id": "UC12345", + "status": "active", + "followers_count": 1000, + "account_url": "https://youtube.com/test_channel", + "description": "测试频道描述", + "tags": "科技,编程,AI", + "profile_image": "https://example.com/new_profile.jpg", + "last_posting": "2023-09-10T15:30:00Z", + "created_at": "2023-09-01T10:00:00Z", + "updated_at": "2023-09-10T15:35:00Z", + "last_login": null + } +} +``` + +## 在Apifox中使用的提示 + +1. **创建平台账号**: + - 使用`POST /api/operation/platforms/`端点 + - 根据需要添加标签、头像和最后发布时间字段 + - 确保operator字段设置为有效的运营账号ID + +2. **更新平台账号资料**: + - 可以使用标准的`PUT /api/operation/platforms/{id}/`端点更新所有字段 + - 或者使用专门的`POST /api/operation/platforms/{id}/update_profile/`端点仅更新资料相关字段 + - 日期时间格式应为ISO 8601格式(如`2023-09-10T15:30:00Z`) + +3. **获取平台账号详情**: + - 使用`GET /api/operation/platforms/{id}/`端点 + - 响应中将包含所有字段,包括新增的标签、头像和最后发布时间 \ No newline at end of file diff --git a/operation/pagination.py b/operation/pagination.py new file mode 100644 index 0000000..febb3cc --- /dev/null +++ b/operation/pagination.py @@ -0,0 +1,23 @@ +from rest_framework.pagination import PageNumberPagination +from rest_framework.response import Response + +class CustomPagination(PageNumberPagination): + """自定义分页器,返回格式为 {code, message, data}""" + page_size = 10 + page_size_query_param = 'page_size' + max_page_size = 100 + + def get_paginated_response(self, data): + return Response({ + "code": 200, + "message": "获取数据成功", + "data": { + "count": self.page.paginator.count, + "next": self.get_next_link(), + "previous": self.get_previous_link(), + "results": data, + "page": self.page.number, + "pages": self.page.paginator.num_pages, + "page_size": self.page_size + } + }) \ No newline at end of file diff --git a/operation/serializers.py b/operation/serializers.py index c161143..bef24ac 100644 --- a/operation/serializers.py +++ b/operation/serializers.py @@ -34,6 +34,7 @@ class PlatformAccountSerializer(serializers.ModelSerializer): model = PlatformAccount fields = ['id', 'operator', 'operator_name', 'platform_name', 'account_name', 'account_id', 'status', 'followers_count', 'account_url', 'description', + 'tags', 'profile_image', 'last_posting', 'created_at', 'updated_at', 'last_login'] read_only_fields = ['id', 'created_at', 'updated_at'] diff --git a/operation/views.py b/operation/views.py index 7b8120e..e17d2f7 100644 --- a/operation/views.py +++ b/operation/views.py @@ -18,6 +18,7 @@ from .serializers import ( OperatorAccountSerializer, PlatformAccountSerializer, VideoSerializer, KnowledgeBaseSerializer, KnowledgeBaseDocumentSerializer ) +from .pagination import CustomPagination logger = logging.getLogger(__name__) @@ -26,7 +27,54 @@ class OperatorAccountViewSet(viewsets.ModelViewSet): queryset = OperatorAccount.objects.all() serializer_class = OperatorAccountSerializer permission_classes = [IsAuthenticated] + pagination_class = CustomPagination + def list(self, request, *args, **kwargs): + """获取运营账号列表""" + queryset = self.filter_queryset(self.get_queryset()) + page = self.paginate_queryset(queryset) + + if page is not None: + serializer = self.get_serializer(page, many=True) + # 使用自定义分页器的响应 + return self.get_paginated_response(serializer.data) + + serializer = self.get_serializer(queryset, many=True) + return Response({ + "code": 200, + "message": "获取运营账号列表成功", + "data": serializer.data + }) + + def retrieve(self, request, *args, **kwargs): + """获取运营账号详情""" + instance = self.get_object() + serializer = self.get_serializer(instance) + return Response({ + "code": 200, + "message": "获取运营账号详情成功", + "data": serializer.data + }) + + def update(self, request, *args, **kwargs): + """更新运营账号信息""" + partial = kwargs.pop('partial', False) + instance = self.get_object() + serializer = self.get_serializer(instance, data=request.data, partial=partial) + serializer.is_valid(raise_exception=True) + self.perform_update(serializer) + + return Response({ + "code": 200, + "message": "更新运营账号信息成功", + "data": serializer.data + }) + + def partial_update(self, request, *args, **kwargs): + """部分更新运营账号信息""" + kwargs['partial'] = True + return self.update(request, *args, **kwargs) + def create(self, request, *args, **kwargs): """创建运营账号并自动创建对应的私有知识库""" with transaction.atomic(): @@ -147,6 +195,53 @@ class PlatformAccountViewSet(viewsets.ModelViewSet): queryset = PlatformAccount.objects.all() serializer_class = PlatformAccountSerializer permission_classes = [IsAuthenticated] + pagination_class = CustomPagination + + def list(self, request, *args, **kwargs): + """获取平台账号列表""" + queryset = self.filter_queryset(self.get_queryset()) + page = self.paginate_queryset(queryset) + + if page is not None: + serializer = self.get_serializer(page, many=True) + # 使用自定义分页器的响应 + return self.get_paginated_response(serializer.data) + + serializer = self.get_serializer(queryset, many=True) + return Response({ + "code": 200, + "message": "获取平台账号列表成功", + "data": serializer.data + }) + + def retrieve(self, request, *args, **kwargs): + """获取平台账号详情""" + instance = self.get_object() + serializer = self.get_serializer(instance) + return Response({ + "code": 200, + "message": "获取平台账号详情成功", + "data": serializer.data + }) + + def update(self, request, *args, **kwargs): + """更新平台账号信息""" + partial = kwargs.pop('partial', False) + instance = self.get_object() + serializer = self.get_serializer(instance, data=request.data, partial=partial) + serializer.is_valid(raise_exception=True) + self.perform_update(serializer) + + return Response({ + "code": 200, + "message": "更新平台账号信息成功", + "data": serializer.data + }) + + def partial_update(self, request, *args, **kwargs): + """部分更新平台账号信息""" + kwargs['partial'] = True + return self.update(request, *args, **kwargs) def create(self, request, *args, **kwargs): """创建平台账号并记录到知识库""" @@ -216,6 +311,9 @@ class PlatformAccountViewSet(viewsets.ModelViewSet): 粉丝数: {platform_account.followers_count} 账号链接: {platform_account.account_url} 账号描述: {platform_account.description or '无'} + 标签: {platform_account.tags or '无'} + 头像链接: {platform_account.profile_image or '无'} + 最后发布时间: {platform_account.last_posting.strftime('%Y-%m-%d %H:%M:%S') if platform_account.last_posting else '未发布'} 创建时间: {platform_account.created_at.strftime('%Y-%m-%d %H:%M:%S')} 最后登录: {platform_account.last_login.strftime('%Y-%m-%d %H:%M:%S') if platform_account.last_login else '从未登录'} """, @@ -343,12 +441,127 @@ class PlatformAccountViewSet(viewsets.ModelViewSet): } }) + @action(detail=True, methods=['post']) + def update_profile(self, request, pk=None): + """更新平台账号的头像、标签和最后发布时间""" + platform_account = self.get_object() + + # 获取更新的资料数据 + profile_data = {} + + # 处理标签 + if 'tags' in request.data: + profile_data['tags'] = request.data['tags'] + + # 处理头像 + if 'profile_image' in request.data: + profile_data['profile_image'] = request.data['profile_image'] + + # 处理最后发布时间 + if 'last_posting' in request.data: + try: + # 尝试解析时间字符串 + from dateutil import parser + last_posting = parser.parse(request.data['last_posting']) + profile_data['last_posting'] = last_posting + except Exception as e: + return Response({ + "code": 400, + "message": f"最后发布时间格式错误: {str(e)}", + "data": None + }, status=status.HTTP_400_BAD_REQUEST) + + if not profile_data: + return Response({ + "code": 400, + "message": "没有提供任何需更新的资料数据", + "data": None + }, status=status.HTTP_400_BAD_REQUEST) + + # 更新平台账号资料 + for field, value in profile_data.items(): + setattr(platform_account, field, value) + platform_account.save() + + # 同步到知识库 + # 在实际应用中应该调用外部API更新文档内容 + operator = platform_account.operator + knowledge_base = KnowledgeBase.objects.filter( + name__contains=operator.real_name, + type='private' + ).first() + + if knowledge_base: + document = KnowledgeBaseDocument.objects.filter( + knowledge_base=knowledge_base, + status='active' + ).filter( + Q(document_name__contains=platform_account.account_name) | + Q(document_name__contains=platform_account.platform_name) + ).first() + + if document: + logger.info(f"应当更新文档 {document.document_id} 的平台账号资料数据") + + return Response({ + "code": 200, + "message": "平台账号资料更新成功", + "data": self.get_serializer(platform_account).data + }) + class VideoViewSet(viewsets.ModelViewSet): """视频管理视图集""" queryset = Video.objects.all() serializer_class = VideoSerializer permission_classes = [IsAuthenticated] + pagination_class = CustomPagination + + def list(self, request, *args, **kwargs): + """获取视频列表""" + queryset = self.filter_queryset(self.get_queryset()) + page = self.paginate_queryset(queryset) + + if page is not None: + serializer = self.get_serializer(page, many=True) + # 使用自定义分页器的响应 + return self.get_paginated_response(serializer.data) + + serializer = self.get_serializer(queryset, many=True) + return Response({ + "code": 200, + "message": "获取视频列表成功", + "data": serializer.data + }) + + def retrieve(self, request, *args, **kwargs): + """获取视频详情""" + instance = self.get_object() + serializer = self.get_serializer(instance) + return Response({ + "code": 200, + "message": "获取视频详情成功", + "data": serializer.data + }) + + def update(self, request, *args, **kwargs): + """更新视频信息""" + partial = kwargs.pop('partial', False) + instance = self.get_object() + serializer = self.get_serializer(instance, data=request.data, partial=partial) + serializer.is_valid(raise_exception=True) + self.perform_update(serializer) + + return Response({ + "code": 200, + "message": "更新视频信息成功", + "data": serializer.data + }) + + def partial_update(self, request, *args, **kwargs): + """部分更新视频信息""" + kwargs['partial'] = True + return self.update(request, *args, **kwargs) def create(self, request, *args, **kwargs): """创建视频并记录到知识库""" @@ -824,29 +1037,55 @@ class VideoViewSet(viewsets.ModelViewSet): "data": None }, status=status.HTTP_400_BAD_REQUEST) - # 执行发布任务 + # 自动发布 - 不依赖Celery任务 try: - from user_management.tasks import publish_scheduled_video - result = publish_scheduled_video(video.id) + # 模拟上传到平台 + platform_account = video.platform_account + platform_name = platform_account.platform_name - if isinstance(result, dict) and result.get('success', False): - return Response({ - "code": 200, - "message": "视频发布成功", - "data": { - "id": video.id, - "title": video.title, - "status": "published", - "video_url": result.get('video_url'), - "publish_time": result.get('publish_time') - } - }) - else: - return Response({ - "code": 500, - "message": f"发布失败: {result.get('error', '未知错误')}", - "data": None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + # 创建模拟的视频URL和ID + video_url = f"https://example.com/{platform_name}/{video.id}" + video_id = f"VID_{video.id}" + + # 更新视频状态 + video.status = 'published' + video.publish_time = timezone.now() + video.video_url = video_url + video.video_id = video_id + video.save() + + logger.info(f"视频 {video.id} 已手动发布") + + # 更新知识库文档 + platform_account = video.platform_account + operator = platform_account.operator + + knowledge_base = KnowledgeBase.objects.filter( + name__contains=operator.real_name, + type='private' + ).first() + + if knowledge_base: + document = KnowledgeBaseDocument.objects.filter( + knowledge_base=knowledge_base, + document_name__contains=video.title, + status='active' + ).first() + + if document: + logger.info(f"应当更新文档 {document.document_id} 的视频发布状态") + + return Response({ + "code": 200, + "message": "视频发布成功", + "data": { + "id": video.id, + "title": video.title, + "status": "published", + "video_url": video_url, + "publish_time": video.publish_time.strftime("%Y-%m-%d %H:%M:%S") + } + }) except Exception as e: logger.error(f"手动发布视频失败: {str(e)}") diff --git a/user_management/migrations/0007_platformaccount_last_posting_and_more.py b/user_management/migrations/0007_platformaccount_last_posting_and_more.py new file mode 100644 index 0000000..b5c6448 --- /dev/null +++ b/user_management/migrations/0007_platformaccount_last_posting_and_more.py @@ -0,0 +1,28 @@ +# Generated by Django 5.1.5 on 2025-04-29 06:52 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('user_management', '0006_rename_credentials_json_gmailcredential_credentials'), + ] + + operations = [ + migrations.AddField( + model_name='platformaccount', + name='last_posting', + field=models.DateTimeField(blank=True, null=True, verbose_name='最后发布时间'), + ), + migrations.AddField( + model_name='platformaccount', + name='profile_image', + field=models.URLField(blank=True, null=True, verbose_name='头像URL'), + ), + migrations.AddField( + model_name='platformaccount', + name='tags', + field=models.CharField(blank=True, help_text='用逗号分隔的标签列表', max_length=255, null=True, verbose_name='标签'), + ), + ] diff --git a/user_management/models.py b/user_management/models.py index 52841bd..fe91e66 100644 --- a/user_management/models.py +++ b/user_management/models.py @@ -871,6 +871,12 @@ class PlatformAccount(models.Model): followers_count = models.IntegerField(default=0, verbose_name='粉丝数') account_url = models.URLField(verbose_name='账号链接') description = models.TextField(blank=True, null=True, verbose_name='账号描述') + + # 新增字段 + tags = models.CharField(max_length=255, blank=True, null=True, verbose_name='标签', help_text='用逗号分隔的标签列表') + profile_image = models.URLField(blank=True, null=True, verbose_name='头像URL') + last_posting = models.DateTimeField(blank=True, null=True, verbose_name='最后发布时间') + created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') updated_at = models.DateTimeField(auto_now=True, verbose_name='更新时间') last_login = models.DateTimeField(blank=True, null=True, verbose_name='最后登录时间')