diff --git a/apps/brands/consumers.py b/apps/brands/consumers.py new file mode 100644 index 0000000..b9da3eb --- /dev/null +++ b/apps/brands/consumers.py @@ -0,0 +1,182 @@ +import json +import logging +from channels.generic.websocket import WebsocketConsumer +from channels.layers import get_channel_layer +from asgiref.sync import async_to_sync +from .services.status_polling_service import polling_service +from .models import Campaign +from apps.daren_detail.models import CreatorCampaign, CreatorProfile + +logger = logging.getLogger('brands') + +class CampaignStatusConsumer(WebsocketConsumer): + """处理活动状态更新的WebSocket消费者""" + + def connect(self): + """处理WebSocket连接请求""" + # 获取活动ID从URL路由参数 + self.campaign_id = self.scope['url_route']['kwargs']['campaign_id'] + self.group_name = f'campaign_{self.campaign_id}' + + # 将连接添加到频道组 + async_to_sync(self.channel_layer.group_add)( + self.group_name, + self.channel_name + ) + + # 接受WebSocket连接 + self.accept() + + # 发送初始状态 + self.send_initial_status() + + # 启动轮询 + self.start_status_polling() + + logger.info(f"WebSocket连接已建立: {self.group_name}") + + def disconnect(self, close_code): + """处理WebSocket断开连接""" + # 将连接从频道组移除 + async_to_sync(self.channel_layer.group_discard)( + self.group_name, + self.channel_name + ) + + logger.info(f"WebSocket连接已断开: {self.group_name}, 关闭代码: {close_code}") + + def receive(self, text_data): + """处理从WebSocket客户端接收的消息""" + try: + # 解析接收到的JSON数据 + data = json.loads(text_data) + action = data.get('action') + + # 处理刷新请求 + if action == 'refresh': + self.send_initial_status() + + logger.debug(f"接收到WebSocket消息: {text_data}") + + except json.JSONDecodeError: + logger.error(f"接收到无效的JSON数据: {text_data}") + except Exception as e: + logger.error(f"处理WebSocket消息时出错: {str(e)}") + + def send_update(self, event): + """向WebSocket客户端发送更新消息""" + # 直接转发消息 + self.send(text_data=event['message']) + + def get_creator_data(self): + """获取创作者数据列表""" + try: + # 查询活动信息 + campaign = Campaign.objects.get(id=self.campaign_id) + + # 查询活动关联的达人 + creator_campaigns = CreatorCampaign.objects.filter( + campaign_id=campaign.id + ).select_related('creator') + + # 构建达人列表数据 + creator_list = [] + for cc in creator_campaigns: + creator = cc.creator + + # 格式化粉丝数和观看量 + followers_formatted = f"{int(creator.followers / 1000)}k" if creator.followers else "0" + avg_views_formatted = f"{int(creator.avg_video_views / 1000)}k" if creator.avg_video_views else "0" + + # 构建响应数据 + creator_data = { + "id": str(creator.id), + "name": creator.name, + "avatar": creator.avatar_url, + "category": creator.category, + "followers": followers_formatted, + "views": avg_views_formatted, + "gmv": f"${creator.gmv}k" if creator.gmv else "$0", + "pricing": f"${creator.pricing_min}" if creator.pricing_min else "$0", + "status": cc.status + } + creator_list.append(creator_data) + + return creator_list + except Exception as e: + logger.error(f"获取创作者数据出错: {str(e)}") + return [] + + def send_initial_status(self): + """发送初始状态信息""" + try: + # 获取创作者数据 + creator_list = self.get_creator_data() + + # 构建并发送标准格式消息 + message = { + 'code': 200, + 'message': '获取成功', + 'data': creator_list + } + + self.send(text_data=json.dumps(message)) + + except Campaign.DoesNotExist: + logger.error(f"找不到活动: {self.campaign_id}") + message = { + 'code': 404, + 'message': '找不到活动', + 'data': None + } + self.send(text_data=json.dumps(message)) + + except Exception as e: + logger.error(f"发送初始状态时出错: {str(e)}") + message = { + 'code': 500, + 'message': f'服务器错误: {str(e)}', + 'data': None + } + self.send(text_data=json.dumps(message)) + + def start_status_polling(self): + """启动状态轮询""" + try: + # 查询活动信息 + campaign = Campaign.objects.get(id=self.campaign_id) + + # 查询活动关联的达人 + creator_campaigns = CreatorCampaign.objects.filter( + campaign_id=campaign.id + ).select_related('creator') + + # 获取产品ID + product_id = None + if campaign.link_product.exists(): + product = campaign.link_product.first() + product_id = product.id + + # 如果没有关联产品,则使用活动ID作为产品ID + if not product_id: + product_id = campaign.id + + # 构建达人-产品对 + creator_product_pairs = [] + for cc in creator_campaigns: + creator_id = cc.creator_id + creator_product_pairs.append((creator_id, product_id)) + + # 启动轮询 + if creator_product_pairs: + polling_service.start_polling( + campaign_id=self.campaign_id, + creator_product_pairs=creator_product_pairs, + interval=30 # 每30秒轮询一次 + ) + logger.info(f"已启动活动 {self.campaign_id} 的状态轮询") + + except Campaign.DoesNotExist: + logger.error(f"找不到活动: {self.campaign_id}") + except Exception as e: + logger.error(f"启动状态轮询时出错: {str(e)}") \ No newline at end of file diff --git a/apps/brands/routing.py b/apps/brands/routing.py new file mode 100644 index 0000000..f1c4fd4 --- /dev/null +++ b/apps/brands/routing.py @@ -0,0 +1,7 @@ +from django.urls import re_path + +from . import consumers + +websocket_urlpatterns = [ + re_path(r'ws/campaigns/(?P\w+)/status/$', consumers.CampaignStatusConsumer.as_asgi()), +] \ No newline at end of file diff --git a/apps/brands/services/__init__.py b/apps/brands/services/__init__.py index 0519ecb..4725f9b 100644 --- a/apps/brands/services/__init__.py +++ b/apps/brands/services/__init__.py @@ -1 +1 @@ - \ No newline at end of file +# 服务模块初始化 \ No newline at end of file diff --git a/apps/brands/services/offer_status_service.py b/apps/brands/services/offer_status_service.py new file mode 100644 index 0000000..53851c4 --- /dev/null +++ b/apps/brands/services/offer_status_service.py @@ -0,0 +1,163 @@ +import requests +import logging +import json +from django.conf import settings +from asgiref.sync import async_to_sync +from channels.layers import get_channel_layer + +logger = logging.getLogger('brands') + +class OfferStatusService: + """提供获取达人谈判状态的服务""" + + @staticmethod + def fetch_status(creator_id, product_id): + """ + 获取达人对产品的谈判状态 + :param creator_id: 达人ID + :param product_id: 产品ID + :return: 状态字符串 + """ + try: + url = "http://127.0.0.1:8000/api/operation/negotiations/offer_status/" + + payload = { + 'creator_id': str(creator_id), + 'product_id': str(product_id) + } + + response = requests.post(url, data=payload) + + if response.status_code == 200: + data = response.json() + if data['code'] == 200: + return data['data']['status'] + else: + logger.error(f"获取谈判状态失败: {data['message']}") + return None + else: + logger.error(f"请求谈判状态接口失败: {response.status_code}") + return None + + except Exception as e: + logger.error(f"获取谈判状态时发生错误: {str(e)}") + return None + + @staticmethod + def update_creator_status(campaign_id, creator_id, status): + """ + 更新达人的状态 + :param campaign_id: 活动ID + :param creator_id: 达人ID + :param status: 新状态 + :return: 是否更新成功 + """ + try: + from apps.daren_detail.models import CreatorCampaign + + # 更新数据库中的状态 + creator_campaign = CreatorCampaign.objects.get( + campaign_id=campaign_id, + creator_id=creator_id + ) + + # 如果状态没有变化,则不进行更新 + if creator_campaign.status == status: + return False + + creator_campaign.status = status + creator_campaign.save() + logger.info(f"已更新数据库中的状态: 活动 {campaign_id}, 达人 {creator_id}, 状态 {status}") + return True + + except CreatorCampaign.DoesNotExist: + logger.error(f"找不到关联记录: 活动 {campaign_id}, 达人 {creator_id}") + return False + except Exception as e: + logger.error(f"更新达人状态时发生错误: {str(e)}") + return False + + @staticmethod + def get_campaign_creator_data(campaign_id): + """ + 获取活动关联的所有达人信息 + :param campaign_id: 活动ID + :return: 达人信息列表 + """ + try: + from apps.daren_detail.models import CreatorCampaign, CreatorProfile + + # 查询与活动关联的所有达人关联记录 + creator_campaigns = CreatorCampaign.objects.filter( + campaign_id=campaign_id + ).select_related('creator') + + creator_list = [] + for cc in creator_campaigns: + creator = cc.creator + + # 格式化粉丝数和观看量 + followers_formatted = f"{int(creator.followers / 1000)}k" if creator.followers else "0" + avg_views_formatted = f"{int(creator.avg_video_views / 1000)}k" if creator.avg_video_views else "0" + + # 构建响应数据 + creator_data = { + "id": str(creator.id), + "name": creator.name, + "avatar": creator.avatar_url, + "category": creator.category, + "followers": followers_formatted, + "views": avg_views_formatted, + "gmv": f"${creator.gmv}k" if creator.gmv else "$0", + "pricing": f"${creator.pricing_min}" if creator.pricing_min else "$0", + "status": cc.status + } + creator_list.append(creator_data) + + return creator_list + + except Exception as e: + logger.error(f"获取活动达人数据时发生错误: {str(e)}") + return [] + + @staticmethod + def send_status_update(campaign_id, creator_id, status): + """ + 通过WebSocket发送状态更新 + :param campaign_id: 活动ID + :param creator_id: 达人ID + :param status: 状态 + """ + try: + # 先更新数据库中的状态 + updated = OfferStatusService.update_creator_status(campaign_id, creator_id, status) + + # 如果状态没有变化,则不发送更新 + if not updated: + return + + # 获取最新的所有达人数据 + creator_list = OfferStatusService.get_campaign_creator_data(campaign_id) + + channel_layer = get_channel_layer() + + # 构建消息数据 - 使用标准的API响应格式 + message = { + 'code': 200, + 'message': '状态已更新', + 'data': creator_list + } + + # 发送到活动特定的群组 + async_to_sync(channel_layer.group_send)( + f'campaign_{campaign_id}', + { + 'type': 'send_update', + 'message': json.dumps(message) + } + ) + + logger.info(f"已发送状态更新: 活动 {campaign_id}, 达人 {creator_id}, 状态 {status}") + + except Exception as e: + logger.error(f"发送WebSocket更新失败: {str(e)}") \ No newline at end of file diff --git a/apps/brands/services/status_polling_service.py b/apps/brands/services/status_polling_service.py new file mode 100644 index 0000000..a987e91 --- /dev/null +++ b/apps/brands/services/status_polling_service.py @@ -0,0 +1,101 @@ +import threading +import time +import logging +from django.db import close_old_connections +from .offer_status_service import OfferStatusService + +logger = logging.getLogger('brands') + +class StatusPollingService: + """提供定时轮询服务,定期获取并更新达人状态""" + + def __init__(self): + self._polling_threads = {} # 保存活动ID到线程的映射 + self._stop_events = {} # 保存活动ID到停止事件的映射 + + def start_polling(self, campaign_id, creator_product_pairs, interval=30): + """ + 开始轮询指定活动的达人状态 + :param campaign_id: 活动ID + :param creator_product_pairs: 达人ID和产品ID的对应关系列表,格式为 [(creator_id, product_id), ...] + :param interval: 轮询间隔时间(秒) + """ + # 如果该活动已有轮询线程,先停止它 + if campaign_id in self._polling_threads: + self.stop_polling(campaign_id) + + # 创建停止事件 + stop_event = threading.Event() + self._stop_events[campaign_id] = stop_event + + # 创建并启动轮询线程 + thread = threading.Thread( + target=self._polling_worker, + args=(campaign_id, creator_product_pairs, interval, stop_event), + daemon=True + ) + self._polling_threads[campaign_id] = thread + thread.start() + + logger.info(f"已启动活动 {campaign_id} 的状态轮询,间隔 {interval} 秒") + + def stop_polling(self, campaign_id): + """ + 停止指定活动的轮询 + :param campaign_id: 活动ID + """ + if campaign_id in self._stop_events: + # 设置停止事件 + self._stop_events[campaign_id].set() + + # 等待线程结束 + if campaign_id in self._polling_threads: + self._polling_threads[campaign_id].join(timeout=5) + + # 清理资源 + del self._polling_threads[campaign_id] + + del self._stop_events[campaign_id] + logger.info(f"已停止活动 {campaign_id} 的状态轮询") + + def stop_all(self): + """停止所有轮询""" + campaign_ids = list(self._polling_threads.keys()) + for campaign_id in campaign_ids: + self.stop_polling(campaign_id) + + def _polling_worker(self, campaign_id, creator_product_pairs, interval, stop_event): + """ + 轮询工作线程 + :param campaign_id: 活动ID + :param creator_product_pairs: 达人ID和产品ID的对应关系列表 + :param interval: 轮询间隔(秒) + :param stop_event: 停止事件 + """ + while not stop_event.is_set(): + try: + # 关闭旧的数据库连接 + close_old_connections() + + # 遍历每个达人-产品对,获取并发送状态更新 + for creator_id, product_id in creator_product_pairs: + try: + # 获取状态 + status = OfferStatusService.fetch_status(creator_id, product_id) + + if status: + # 发送状态更新 + OfferStatusService.send_status_update(campaign_id, creator_id, status) + except Exception as e: + logger.error(f"处理达人 {creator_id} 状态时出错: {str(e)}") + + # 等待指定时间间隔 + stop_event.wait(interval) + + except Exception as e: + logger.error(f"轮询线程发生错误: {str(e)}") + # 短暂休眠后继续 + time.sleep(5) + +# 创建单例实例 +polling_service = StatusPollingService() \ No newline at end of file diff --git a/apps/brands/views.py b/apps/brands/views.py index 4a282fc..16aa871 100644 --- a/apps/brands/views.py +++ b/apps/brands/views.py @@ -2,6 +2,7 @@ from django.shortcuts import render, get_object_or_404 from rest_framework import viewsets, status from rest_framework.decorators import action from rest_framework.response import Response +import logging from .models import Brand, Product, Campaign, BrandChatSession from .serializers import ( @@ -11,6 +12,10 @@ from .serializers import ( BrandChatSessionSerializer, BrandDetailSerializer ) +from .services.status_polling_service import polling_service +from .services.offer_status_service import OfferStatusService + +logger = logging.getLogger(__name__) def api_response(code=200, message="成功", data=None): """统一API响应格式""" @@ -269,6 +274,99 @@ class CampaignViewSet(viewsets.ModelViewSet): except Exception as e: return api_response(code=500, message=f"移除产品失败: {str(e)}", data=None) + @action(detail=True, methods=['get']) + def creator_list(self, request, pk=None): + """获取活动关联的达人列表""" + campaign = self.get_object() + from apps.daren_detail.models import CreatorCampaign, CreatorProfile + + # 获取所有达人数据 + creator_list = OfferStatusService.get_campaign_creator_data(campaign.id) + + # 启动状态轮询(当有用户请求此接口时) + try: + # 获取产品ID + product_id = None + if campaign.link_product.exists(): + product = campaign.link_product.first() + product_id = product.id + + # 如果没有关联产品,则使用活动ID作为产品ID + if not product_id: + product_id = campaign.id + + # 构建达人-产品对 + creator_product_pairs = [] + for creator_data in creator_list: + creator_id = creator_data['id'] + creator_product_pairs.append((creator_id, product_id)) + + # 启动轮询 + if creator_product_pairs: + polling_service.start_polling( + campaign_id=campaign.id, + creator_product_pairs=creator_product_pairs, + interval=30 # 每30秒轮询一次 + ) + except Exception as e: + logger.error(f"启动状态轮询时出错: {str(e)}") + + return api_response(data=creator_list) + + @action(detail=True, methods=['post']) + def update_creator_status(self, request, pk=None): + """手动更新达人状态""" + campaign = self.get_object() + from apps.daren_detail.models import CreatorCampaign + from .services.offer_status_service import OfferStatusService + + # 获取传入的达人ID + creator_id = request.data.get('creator_id') + + if not creator_id: + return api_response(code=400, message="缺少必要参数: creator_id", data=None) + + try: + # 查询达人与活动的关联 + creator_campaign = CreatorCampaign.objects.get( + campaign_id=campaign.id, + creator_id=creator_id + ) + + # 获取产品ID + product_id = None + if campaign.link_product.exists(): + product = campaign.link_product.first() + product_id = product.id + + # 如果没有关联产品,则使用活动ID作为产品ID + if not product_id: + product_id = campaign.id + + # 获取最新状态 + status = OfferStatusService.fetch_status(creator_id, product_id) + + if status: + # 更新状态 + creator_campaign.status = status + creator_campaign.save() + + # 获取所有达人的最新数据 + creator_list = OfferStatusService.get_campaign_creator_data(campaign.id) + + # 发送WebSocket更新 + OfferStatusService.send_status_update(campaign.id, creator_id, status) + + return api_response(message="状态已更新", data=creator_list) + else: + return api_response(code=500, message="获取状态失败", data=None) + + except CreatorCampaign.DoesNotExist: + return api_response(code=404, message="找不到达人与活动的关联", data=None) + except Exception as e: + logger.error(f"更新达人状态时出错: {str(e)}") + return api_response(code=500, message=f"更新状态失败: {str(e)}", data=None) + class BrandChatSessionViewSet(viewsets.ModelViewSet): """品牌聊天会话API视图集""" diff --git a/apps/expertproducts/migrations/0002_alter_negotiation_creator_alter_negotiation_product_and_more.py b/apps/expertproducts/migrations/0002_alter_negotiation_creator_alter_negotiation_product_and_more.py new file mode 100644 index 0000000..27f0042 --- /dev/null +++ b/apps/expertproducts/migrations/0002_alter_negotiation_creator_alter_negotiation_product_and_more.py @@ -0,0 +1,29 @@ +# Generated by Django 5.1.5 on 2025-05-20 04:36 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('brands', '0002_alter_campaign_id'), + ('daren_detail', '0001_initial'), + ('expertproducts', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='negotiation', + name='creator', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='daren_detail.creatorprofile'), + ), + migrations.AlterField( + model_name='negotiation', + name='product', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='brands.product'), + ), + migrations.DeleteModel( + name='Creator', + ), + ] diff --git a/apps/expertproducts/migrations/0003_delete_product.py b/apps/expertproducts/migrations/0003_delete_product.py new file mode 100644 index 0000000..10fa121 --- /dev/null +++ b/apps/expertproducts/migrations/0003_delete_product.py @@ -0,0 +1,16 @@ +# Generated by Django 5.1.5 on 2025-05-20 04:37 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('expertproducts', '0002_alter_negotiation_creator_alter_negotiation_product_and_more'), + ] + + operations = [ + migrations.DeleteModel( + name='Product', + ), + ] diff --git a/apps/expertproducts/models.py b/apps/expertproducts/models.py index 9d72faa..01d15a9 100644 --- a/apps/expertproducts/models.py +++ b/apps/expertproducts/models.py @@ -1,28 +1,12 @@ from django.db import models from django.db.models import JSONField # 用于存储动态谈判条款 from django.utils import timezone # Import timezone for timestamping +from apps.daren_detail.models import CreatorProfile # 导入CreatorProfile模型 +from apps.brands.models import Product # 导入Product模型 # Create your models here. -class Product(models.Model): - name = models.CharField(max_length=100) # 商品名称 - category = models.CharField(max_length=50) # 商品类目 - max_price = models.DecimalField(max_digits=10, decimal_places=2) # 最高价格(公开报价) - min_price = models.DecimalField(max_digits=10, decimal_places=2) # 最低价格(底线) - description = models.TextField(blank=True) # 商品描述(可选) - - def __str__(self): - return f"{self.name} ({self.max_price}元)" - -class Creator(models.Model): - name = models.CharField(max_length=100) # 达人名称 - sex = models.CharField(max_length=10, default='男') # 达人性别,设置默认值为未知 - age = models.IntegerField(default=18) # 达人年龄,设置默认值为18 - category = models.CharField(max_length=50) # 达人类别(如带货类) - followers = models.IntegerField() # 粉丝数 - - class Negotiation(models.Model): STATUS_CHOICES = [ ('brand_review', '品牌回顾'), @@ -34,8 +18,8 @@ class Negotiation(models.Model): ('abandoned', '已放弃'), ] - creator = models.ForeignKey('Creator', on_delete=models.CASCADE) - product = models.ForeignKey('Product', on_delete=models.CASCADE) + creator = models.ForeignKey(CreatorProfile, on_delete=models.CASCADE) + product = models.ForeignKey(Product, on_delete=models.CASCADE) status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='price_negotiation') current_round = models.IntegerField(default=1) # 当前谈判轮次 context = models.JSONField(default=dict) # 存储谈判上下文(如当前报价) diff --git a/apps/expertproducts/serializers.py b/apps/expertproducts/serializers.py index 08b439c..79396a6 100644 --- a/apps/expertproducts/serializers.py +++ b/apps/expertproducts/serializers.py @@ -2,8 +2,9 @@ from rest_framework import serializers # from user_management.models import OperatorAccount, PlatformAccount, Video, KnowledgeBase, KnowledgeBaseDocument import uuid from django.db.models import Q - -from .models import Product, Creator, Negotiation, Message +from apps.daren_detail.models import CreatorProfile +from apps.brands.models import Product +from .models import Negotiation, Message # # class OperatorAccountSerializer(serializers.ModelSerializer): @@ -155,16 +156,15 @@ class ProductSerializer(serializers.ModelSerializer): fields = '__all__' -class CreatorSerializer(serializers.ModelSerializer): - class Meta: - model = Creator - fields = '__all__' - - class NegotiationSerializer(serializers.ModelSerializer): + creator_name = serializers.CharField(source='creator.name', read_only=True) + creator_avatar = serializers.CharField(source='creator.avatar_url', read_only=True) + product_name = serializers.CharField(source='product.name', read_only=True) + product_category = serializers.CharField(source='product.category', read_only=True) + class Meta: model = Negotiation - fields = '__all__' + fields = ['id', 'creator', 'creator_name', 'creator_avatar', 'product', 'product_name', 'product_category', 'status', 'current_round', 'context'] read_only_fields = ('status', 'current_round') diff --git a/apps/expertproducts/views.py b/apps/expertproducts/views.py index ecdbb86..242e26f 100644 --- a/apps/expertproducts/views.py +++ b/apps/expertproducts/views.py @@ -17,15 +17,16 @@ import subprocess import re from django.db import connection from django.core.mail import EmailMessage - +from ollama import Client from rest_framework.views import APIView from rest_framework.parsers import MultiPartParser, FormParser -from .models import Product, Creator, Negotiation, Message -from .serializers import ProductSerializer, CreatorSerializer, NegotiationSerializer -import requests -from ollama import Client +from .models import Product, Negotiation, Message +from .serializers import NegotiationSerializer +from apps.daren_detail.models import CreatorProfile +from apps.brands.models import Product + client = Client(host="http://localhost:11434") class ContentAnalysisAPI(APIView): parser_classes = (MultiPartParser, FormParser) @@ -172,7 +173,7 @@ class NegotiationViewSet(viewsets.ModelViewSet): product = serializer.validated_data['product'] # 检查该用户是否存在 - if not Creator.objects.filter(id=creator.id).exists(): + if not CreatorProfile.objects.filter(id=creator.id).exists(): return Response({ "code": 404, "message": "未找到指定的达人", @@ -346,7 +347,7 @@ class NegotiationViewSet(viewsets.ModelViewSet): }) # 获取所有相关的达人 - creators = Creator.objects.filter(negotiation__in=negotiations).distinct() + creators = CreatorProfile.objects.filter(negotiation__in=negotiations).distinct() if creators.exists(): # 序列化达人数据 creator_data = [{'name': creator.name, 'category': creator.category, 'followers': creator.followers} for @@ -362,6 +363,7 @@ class NegotiationViewSet(viewsets.ModelViewSet): @action(detail=False, methods=['post']) def offer_status(self, request): """获取谈判状态""" + # 获取请求参数 creator_id = request.data.get('creator_id') product_id = request.data.get('product_id') @@ -374,7 +376,7 @@ class NegotiationViewSet(viewsets.ModelViewSet): # 查找符合条件的谈判 try: - creator = Creator.objects.get(id=creator_id) + creator = CreatorProfile.objects.get(id=creator_id) negotiation = Negotiation.objects.get(creator=creator, product_id=product_id) return Response({ 'code': 200, @@ -383,6 +385,12 @@ class NegotiationViewSet(viewsets.ModelViewSet): 'status': negotiation.status } }) + except CreatorProfile.DoesNotExist: + return Response({ + 'code': 404, + 'message': f'找不到ID为{creator_id}的达人', + 'data': None + }) except Negotiation.DoesNotExist: return Response({ 'code': 404, diff --git a/daren/asgi.py b/daren/asgi.py index 93a365d..38f9ff0 100644 --- a/daren/asgi.py +++ b/daren/asgi.py @@ -1,16 +1,26 @@ """ ASGI config for daren project. - -It exposes the ASGI callable as a module-level variable named ``application``. - -For more information on this file, see -https://docs.djangoproject.com/en/5.1/howto/deployment/asgi/ """ import os +import django + +# 设置Django设置模块 +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'daren.settings') +django.setup() # 添加这一行 from django.core.asgi import get_asgi_application +from channels.routing import ProtocolTypeRouter, URLRouter +from channels.auth import AuthMiddlewareStack -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'daren.settings') +# 确保在django.setup()之后再导入 +import apps.brands.routing -application = get_asgi_application() +application = ProtocolTypeRouter({ + 'http': get_asgi_application(), + 'websocket': AuthMiddlewareStack( + URLRouter( + apps.brands.routing.websocket_urlpatterns + ) + ), +}) diff --git a/daren/settings.py b/daren/settings.py index 36e8cbc..2604217 100644 --- a/daren/settings.py +++ b/daren/settings.py @@ -15,6 +15,8 @@ import os # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent +import pymysql +pymysql.install_as_MySQLdb() # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/ @@ -38,6 +40,7 @@ INSTALLED_APPS = [ 'django.contrib.messages', 'django_filters', 'django.contrib.staticfiles', + 'channels', 'apps.user.apps.UserConfig', "apps.expertproducts.apps.ExpertproductsConfig", "apps.daren_detail.apps.DarenDetailConfig", @@ -78,6 +81,14 @@ TEMPLATES = [ ] WSGI_APPLICATION = 'daren.wsgi.application' +ASGI_APPLICATION = 'daren.asgi.application' + +# WebSocket配置 +CHANNEL_LAYERS = { + 'default': { + 'BACKEND': 'channels.layers.InMemoryChannelLayer', + }, +} # Database @@ -94,9 +105,9 @@ DATABASES = { 'OPTIONS': { 'charset': 'utf8mb4', 'init_command': "SET sql_mode='STRICT_TRANS_TABLES'", - 'connect_timeout': 60, # 连接超时时间 + 'connect_timeout': 60, }, - 'CONN_MAX_AGE': 0, # 强制Django在每次请求后关闭连接 + 'CONN_MAX_AGE': 0, } } @@ -173,6 +184,16 @@ LOGGING = { 'level': 'INFO', 'propagate': True, }, + 'daren_detail': { + 'handlers': ['console', 'file'], + 'level': 'INFO', + 'propagate': True, + }, + 'brands': { + 'handlers': ['console', 'file'], + 'level': 'INFO', + 'propagate': True, + }, }, } diff --git a/requirements.txt b/requirements.txt index 0fb7d29..b118617 100644 Binary files a/requirements.txt and b/requirements.txt differ