Compare commits

...

2 Commits

Author SHA1 Message Date
jlj
83f8dd931e 合并 2025-05-20 12:40:54 +08:00
jlj
c6dbda5a88 添加部分token 2025-05-20 12:24:53 +08:00
13 changed files with 551 additions and 332 deletions

View File

@ -1373,7 +1373,6 @@ def get_creator_trends(request, creator_id=None):
(datetime.now().date() - timedelta(days=30 - i)).strftime('%Y-%m-%d') (datetime.now().date() - timedelta(days=30 - i)).strftime('%Y-%m-%d')
for i in range(31) for i in range(31)
] ]
# 设定随机的起始值和波动 # 设定随机的起始值和波动
import random import random
base_gmv = random.uniform(500, 3000) base_gmv = random.uniform(500, 3000)

View File

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

View File

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

View File

@ -1,28 +1,12 @@
from django.db import models from django.db import models
from django.db.models import JSONField # 用于存储动态谈判条款 from django.db.models import JSONField # 用于存储动态谈判条款
from django.utils import timezone # Import timezone for timestamping 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. # 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): class Negotiation(models.Model):
STATUS_CHOICES = [ STATUS_CHOICES = [
('brand_review', '品牌回顾'), ('brand_review', '品牌回顾'),
@ -34,8 +18,8 @@ class Negotiation(models.Model):
('abandoned', '已放弃'), ('abandoned', '已放弃'),
] ]
creator = models.ForeignKey('daren_detail.CreatorProfile', on_delete=models.CASCADE) creator = models.ForeignKey(CreatorProfile, on_delete=models.CASCADE)
product = models.ForeignKey('brands.Product', on_delete=models.CASCADE) product = models.ForeignKey(Product, on_delete=models.CASCADE)
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='price_negotiation') status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='price_negotiation')
current_round = models.IntegerField(default=1) # 当前谈判轮次 current_round = models.IntegerField(default=1) # 当前谈判轮次
context = models.JSONField(default=dict) # 存储谈判上下文(如当前报价) context = models.JSONField(default=dict) # 存储谈判上下文(如当前报价)

View File

@ -2,8 +2,9 @@ from rest_framework import serializers
# from user_management.models import OperatorAccount, PlatformAccount, Video, KnowledgeBase, KnowledgeBaseDocument # from user_management.models import OperatorAccount, PlatformAccount, Video, KnowledgeBase, KnowledgeBaseDocument
import uuid import uuid
from django.db.models import Q from django.db.models import Q
from apps.daren_detail.models import CreatorProfile
from .models import Product, Creator, Negotiation, Message from apps.brands.models import Product
from .models import Negotiation, Message
# #
# class OperatorAccountSerializer(serializers.ModelSerializer): # class OperatorAccountSerializer(serializers.ModelSerializer):
@ -155,16 +156,15 @@ class ProductSerializer(serializers.ModelSerializer):
fields = '__all__' fields = '__all__'
class CreatorSerializer(serializers.ModelSerializer):
class Meta:
model = Creator
fields = '__all__'
class NegotiationSerializer(serializers.ModelSerializer): 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: class Meta:
model = Negotiation 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') read_only_fields = ('status', 'current_round')

View File

@ -17,15 +17,16 @@ import subprocess
import re import re
from django.db import connection from django.db import connection
from django.core.mail import EmailMessage from django.core.mail import EmailMessage
from ollama import Client
from rest_framework.views import APIView from rest_framework.views import APIView
from rest_framework.parsers import MultiPartParser, FormParser from rest_framework.parsers import MultiPartParser, FormParser
from .models import Product, Creator, Negotiation, Message from .models import Product, Negotiation, Message
from .serializers import ProductSerializer, CreatorSerializer, NegotiationSerializer from .serializers import NegotiationSerializer
import requests from apps.daren_detail.models import CreatorProfile
from ollama import Client from apps.brands.models import Product
client = Client(host="http://localhost:11434") client = Client(host="http://localhost:11434")
class ContentAnalysisAPI(APIView): class ContentAnalysisAPI(APIView):
parser_classes = (MultiPartParser, FormParser) parser_classes = (MultiPartParser, FormParser)
@ -172,7 +173,7 @@ class NegotiationViewSet(viewsets.ModelViewSet):
product = serializer.validated_data['product'] 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({ return Response({
"code": 404, "code": 404,
"message": "未找到指定的达人", "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(): if creators.exists():
# 序列化达人数据 # 序列化达人数据
creator_data = [{'name': creator.name, 'category': creator.category, 'followers': creator.followers} for 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']) @action(detail=False, methods=['post'])
def offer_status(self, request): def offer_status(self, request):
"""获取谈判状态""" """获取谈判状态"""
# 获取请求参数
creator_id = request.data.get('creator_id') creator_id = request.data.get('creator_id')
product_id = request.data.get('product_id') product_id = request.data.get('product_id')
@ -374,7 +376,7 @@ class NegotiationViewSet(viewsets.ModelViewSet):
# 查找符合条件的谈判 # 查找符合条件的谈判
try: 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) negotiation = Negotiation.objects.get(creator=creator, product_id=product_id)
return Response({ return Response({
'code': 200, 'code': 200,
@ -383,6 +385,12 @@ class NegotiationViewSet(viewsets.ModelViewSet):
'status': negotiation.status 'status': negotiation.status
} }
}) })
except CreatorProfile.DoesNotExist:
return Response({
'code': 404,
'message': f'找不到ID为{creator_id}的达人',
'data': None
})
except Negotiation.DoesNotExist: except Negotiation.DoesNotExist:
return Response({ return Response({
'code': 404, 'code': 404,

View File

@ -0,0 +1,42 @@
from rest_framework import authentication
from rest_framework import exceptions
from django.contrib.auth.models import AnonymousUser
from .models import User, UserToken
from django.utils import timezone
class CustomTokenAuthentication(authentication.BaseAuthentication):
keyword = 'Token' # 设置认证头关键字
def authenticate(self, request):
# 从请求头获取token
auth_header = request.META.get('HTTP_AUTHORIZATION')
if not auth_header:
return None
try:
# 提取token
parts = auth_header.split()
if len(parts) != 2 or parts[0] != self.keyword:
raise exceptions.AuthenticationFailed('无效的认证头格式,应为: Token <token>')
token = parts[1]
# 查找token记录并确保token存在且有效
try:
token_obj = UserToken.objects.select_related('user').get(
token=token,
expired_at__gt=timezone.now() # 确保token未过期
)
except UserToken.DoesNotExist:
raise exceptions.AuthenticationFailed('无效的token')
# 检查用户是否激活
if not token_obj.user.is_active:
raise exceptions.AuthenticationFailed('用户已被禁用')
return (token_obj.user, None)
except Exception as e:
raise exceptions.AuthenticationFailed(f'认证失败: {str(e)}')

View File

@ -0,0 +1,27 @@
# Generated by Django 5.1.5 on 2025-05-20 03:49
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('user', '0002_remove_user_is_active'),
]
operations = [
migrations.CreateModel(
name='UserToken',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('token', models.CharField(max_length=40, unique=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('expired_at', models.DateTimeField()),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tokens', to='user.user')),
],
options={
'db_table': 'user_token',
},
),
]

View File

@ -0,0 +1,23 @@
# Generated by Django 5.1.5 on 2025-05-20 03:52
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('user', '0003_usertoken'),
]
operations = [
migrations.AddField(
model_name='user',
name='is_active',
field=models.BooleanField(default=True),
),
migrations.AddField(
model_name='user',
name='is_staff',
field=models.BooleanField(default=False),
),
]

View File

@ -1,7 +1,20 @@
from django.db import models from django.db import models
from django.utils import timezone
from datetime import timedelta
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager
# Create your models here. class UserManager(BaseUserManager):
class User(models.Model): def create_user(self, email, password=None, **extra_fields):
if not email:
raise ValueError('邮箱地址不能为空')
email = self.normalize_email(email)
user = self.model(email=email, **extra_fields)
if password:
user.set_password(password)
user.save(using=self._db)
return user
class User(AbstractBaseUser):
"""用户模型,用于登录和账户管理""" """用户模型,用于登录和账户管理"""
email = models.EmailField(max_length=255, unique=True, verbose_name="电子邮箱") email = models.EmailField(max_length=255, unique=True, verbose_name="电子邮箱")
password = models.CharField(max_length=255, verbose_name="密码") password = models.CharField(max_length=255, verbose_name="密码")
@ -9,11 +22,18 @@ class User(models.Model):
name = models.CharField(max_length=255, blank=True, null=True, verbose_name="用户姓名") name = models.CharField(max_length=255, blank=True, null=True, verbose_name="用户姓名")
is_first_login = models.BooleanField(default=True, verbose_name="是否首次登录") is_first_login = models.BooleanField(default=True, verbose_name="是否首次登录")
last_login = models.DateTimeField(blank=True, null=True, verbose_name="最近登录时间") last_login = models.DateTimeField(blank=True, null=True, verbose_name="最近登录时间")
is_active = models.BooleanField(default=True)
is_staff = models.BooleanField(default=False)
# 时间戳 # 时间戳
created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
updated_at = models.DateTimeField(auto_now=True, verbose_name="更新时间") updated_at = models.DateTimeField(auto_now=True, verbose_name="更新时间")
objects = UserManager()
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = []
class Meta: class Meta:
verbose_name = "用户" verbose_name = "用户"
verbose_name_plural = verbose_name verbose_name_plural = verbose_name
@ -21,3 +41,25 @@ class User(models.Model):
def __str__(self): def __str__(self):
return self.email return self.email
@property
def is_authenticated(self):
return True
class UserToken(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='tokens')
token = models.CharField(max_length=40, unique=True)
created_at = models.DateTimeField(auto_now_add=True)
expired_at = models.DateTimeField()
def save(self, *args, **kwargs):
if not self.expired_at:
# 设置token有效期为30天
self.expired_at = timezone.now() + timedelta(days=30)
super().save(*args, **kwargs)
def is_expired(self):
return timezone.now() > self.expired_at
class Meta:
db_table = 'user_token'

View File

@ -11,6 +11,12 @@ import concurrent.futures
import shutil import shutil
import dotenv import dotenv
import random import random
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated, AllowAny
import hashlib
import time
from django.contrib.auth.hashers import check_password
from django.utils import timezone
dotenv.load_dotenv() dotenv.load_dotenv()
@ -23,14 +29,44 @@ directory_monitoring = {}
monitor_thread = None monitor_thread = None
is_monitoring = False is_monitoring = False
def generate_token(user_id):
"""生成简单的token"""
# 使用用户ID和当前时间戳生成token
token_string = f"{user_id}:{time.time()}"
return hashlib.sha1(token_string.encode()).hexdigest()
def create_user_token(user):
"""创建并保存用户token"""
from .models import UserToken
# 删除该用户的所有旧token
UserToken.objects.filter(user=user).delete()
# 生成新token
token = generate_token(user.id)
# 保存到数据库
user_token = UserToken.objects.create(
user=user,
token=token
)
return token
@csrf_exempt @csrf_exempt
@require_http_methods(["POST"]) @api_view(['POST'])
@permission_classes([AllowAny])
def user_login(request): def user_login(request):
"""用户登录接口,首次登录会返回需要填写信息的标志""" """
用户登录接口
返回的 token 使用格式
在请求头中添加
Authorization: Token <your_token>
例如
Authorization: Token fa6931ec4cf5bd46d8dc3a671fe9862c467426b3
"""
try: try:
from .models import User from .models import User
import json import json
from django.contrib.auth.hashers import check_password, make_password from django.contrib.auth.hashers import check_password
from datetime import datetime from datetime import datetime
data = json.loads(request.body) data = json.loads(request.body)
@ -51,22 +87,21 @@ def user_login(request):
user = User.objects.get(email=email) user = User.objects.get(email=email)
# 验证密码 # 验证密码
# 注意:这里假设密码已经进行了哈希存储,实际使用时需要采用适当的密码验证方法 if not user.check_password(password):
# 如果密码未哈希存储,直接比较原始密码
password_valid = (user.password == password)
if not password_valid:
return JsonResponse({ return JsonResponse({
'code': 401, 'code': 401,
'message': '用户名或密码错误', 'message': '用户名或密码错误',
'data': None 'data': None
}, json_dumps_params={'ensure_ascii': False}) }, json_dumps_params={'ensure_ascii': False})
# 生成并保存token
token = create_user_token(user)
# 检查是否首次登录 # 检查是否首次登录
is_first_login = user.is_first_login is_first_login = user.is_first_login
# 更新最后登录时间 # 更新最后登录时间
user.last_login = datetime.now() user.last_login = timezone.now()
user.save() user.save()
# 构造返回数据 # 构造返回数据
@ -75,7 +110,8 @@ def user_login(request):
'email': user.email, 'email': user.email,
'is_first_login': is_first_login, 'is_first_login': is_first_login,
'name': user.name, 'name': user.name,
'company': user.company 'company': user.company,
'token': token
} }
return JsonResponse({ return JsonResponse({
@ -85,24 +121,10 @@ def user_login(request):
}, json_dumps_params={'ensure_ascii': False}) }, json_dumps_params={'ensure_ascii': False})
except User.DoesNotExist: except User.DoesNotExist:
# 用户不存在,创建新用户
new_user = User.objects.create(
email=email,
password=password, # 注意:实际使用时应该哈希存储密码
is_first_login=True,
last_login=datetime.now()
)
return JsonResponse({ return JsonResponse({
'code': 200, 'code': 404,
'message': '登录成功', 'message': '用户不存在',
'data': { 'data': None
'user_id': new_user.id,
'email': new_user.email,
'is_first_login': True,
'name': None,
'company': None
}
}, json_dumps_params={'ensure_ascii': False}) }, json_dumps_params={'ensure_ascii': False})
except Exception as e: except Exception as e:
@ -117,24 +139,25 @@ def user_login(request):
@csrf_exempt @csrf_exempt
@require_http_methods(["POST"]) @api_view(['POST'])
@permission_classes([IsAuthenticated])
def update_user_info(request): def update_user_info(request):
"""更新用户信息,首次登录时填写公司和姓名""" """更新用户信息,需要认证"""
try: try:
from .models import User
import json
data = json.loads(request.body) data = json.loads(request.body)
# 获取参数 # 获取参数
user_id = data.get('user_id')
company = data.get('company') company = data.get('company')
name = data.get('name') name = data.get('name')
if not user_id: # 获取当前认证用户
user = request.user
# 如果请求中包含 user_id 且与当前用户不匹配,返回错误
if 'user_id' in data and int(data['user_id']) != user.id:
return JsonResponse({ return JsonResponse({
'code': 400, 'code': 403,
'message': '缺少必要参数: user_id', 'message': '您只能修改自己的信息',
'data': None 'data': None
}, json_dumps_params={'ensure_ascii': False}) }, json_dumps_params={'ensure_ascii': False})
@ -146,10 +169,6 @@ def update_user_info(request):
'data': None 'data': None
}, json_dumps_params={'ensure_ascii': False}) }, json_dumps_params={'ensure_ascii': False})
# 查询用户并更新信息
try:
user = User.objects.get(id=user_id)
# 更新信息 # 更新信息
user.company = company user.company = company
user.name = name user.name = name
@ -168,13 +187,6 @@ def update_user_info(request):
} }
}, json_dumps_params={'ensure_ascii': False}) }, json_dumps_params={'ensure_ascii': False})
except User.DoesNotExist:
return JsonResponse({
'code': 404,
'message': f'找不到ID为 {user_id} 的用户',
'data': None
}, json_dumps_params={'ensure_ascii': False})
except Exception as e: except Exception as e:
logger.error(f"更新用户信息失败: {e}") logger.error(f"更新用户信息失败: {e}")
import traceback import traceback
@ -187,9 +199,10 @@ def update_user_info(request):
@csrf_exempt @csrf_exempt
@require_http_methods(["POST"]) @api_view(['POST'])
@permission_classes([AllowAny])
def user_register(request): def user_register(request):
"""用户注册接口,允许用户创建新账户,可选填写公司和姓名""" """用户注册接口"""
try: try:
from .models import User from .models import User
import json import json
@ -225,13 +238,13 @@ def user_register(request):
is_first_login = not (company and name) is_first_login = not (company and name)
# 创建用户 # 创建用户
user = User.objects.create( user = User.objects.create_user(
email=email, email=email,
password=password, # 注意:实际使用时应该哈希存储密码 password=password,
company=company, company=company,
name=name, name=name,
is_first_login=is_first_login, is_first_login=is_first_login,
last_login=datetime.now() last_login=timezone.now()
) )
# 构造返回数据 # 构造返回数据

View File

@ -41,14 +41,14 @@ INSTALLED_APPS = [
'django_filters', 'django_filters',
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'channels', 'channels',
'rest_framework',
'apps.user.apps.UserConfig', 'apps.user.apps.UserConfig',
"apps.expertproducts.apps.ExpertproductsConfig", "apps.expertproducts.apps.ExpertproductsConfig",
"apps.daren_detail.apps.DarenDetailConfig", "apps.daren_detail.apps.DarenDetailConfig",
"apps.discovery.apps.DiscoveryConfig", "apps.discovery.apps.DiscoveryConfig",
"apps.template.apps.TemplateConfig", "apps.template.apps.TemplateConfig",
"apps.brands.apps.BrandsConfig", "apps.brands.apps.BrandsConfig",
'rest_framework',
'rest_framework_simplejwt',
] ]
MIDDLEWARE = [ MIDDLEWARE = [
@ -196,3 +196,33 @@ LOGGING = {
}, },
}, },
} }
# 自定义用户模型
AUTH_USER_MODEL = 'user.User'
# REST Framework 设置
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'apps.user.authentication.CustomTokenAuthentication',
),
}
# JWT 设置
from datetime import timedelta
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(days=1),
'REFRESH_TOKEN_LIFETIME': timedelta(days=7),
'ROTATE_REFRESH_TOKENS': False,
'ALGORITHM': 'HS256',
'SIGNING_KEY': SECRET_KEY,
'AUTH_HEADER_TYPES': ('Bearer',),
'USER_ID_FIELD': 'id',
'USER_ID_CLAIM': 'user_id',
'UPDATE_LAST_LOGIN': False, # 不在 token 中记录最后登录时间
'TOKEN_TYPE_CLAIM': None, # 不在 token 中包含 token 类型
'JTI_CLAIM': None, # 不在 token 中包含 JWT ID
}
# JWT配置
JWT_SECRET_KEY = 'your-secret-key-here' # 建议使用更安全的密钥
JWT_EXPIRATION_DELTA = 24 * 60 * 60 # token有效期24小时

View File

@ -1,8 +1,14 @@
from django.contrib import admin from django.contrib import admin
from django.urls import path, include from django.urls import path, include
from rest_framework_simplejwt.views import (
TokenObtainPairView,
TokenRefreshView,
)
urlpatterns = [ urlpatterns = [
path('admin/', admin.site.urls), path('admin/', admin.site.urls),
path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
path('api/user/', include('apps.user.urls')), path('api/user/', include('apps.user.urls')),
path('api/daren_detail/', include('apps.daren_detail.urls')), path('api/daren_detail/', include('apps.daren_detail.urls')),
path('api/operation/', include('apps.expertproducts.urls')), path('api/operation/', include('apps.expertproducts.urls')),