修改权限

This commit is contained in:
jlj 2025-05-23 19:08:40 +08:00
parent 4a2e7f1222
commit b68062051a
16 changed files with 1957 additions and 91 deletions

211
AVATAR_SETUP_README.md Normal file
View File

@ -0,0 +1,211 @@
# CreatorProfile头像字段本地图片支持方案
## 问题解答
**问题**: `avatar_url` 字段可以使用本地的图片显示吗?
**答案**: 可以我已经为您实现了一个完整的解决方案支持本地图片上传和外部URL两种方式。
## 解决方案概述
### 1. 模型更改
`CreatorProfile` 模型中添加了新的 `avatar` 字段:
```python
# apps/daren_detail/models.py
class CreatorProfile(models.Model):
# 原有字段
name = models.CharField(max_length=255, verbose_name="达人名称")
# 新增:支持本地图片上传
avatar = models.ImageField(upload_to='avatars/', blank=True, null=True, verbose_name="头像图片")
# 保留外部URL支持向后兼容
avatar_url = models.TextField(blank=True, null=True, verbose_name="头像URL")
def get_avatar_url(self):
"""获取头像URL优先返回本地图片其次返回外部URL"""
if self.avatar:
return self.avatar.url
elif self.avatar_url:
return self.avatar_url
return None
```
### 2. Django设置配置
添加了媒体文件支持:
```python
# daren/settings.py
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
```
```python
# daren/urls.py
from django.conf import settings
from django.conf.urls.static import static
# 在开发环境中提供媒体文件服务
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
```
### 3. 序列化器支持
创建了专门的序列化器来处理头像字段:
```python
# apps/daren_detail/serializers.py
class CreatorProfileSerializer(serializers.ModelSerializer):
avatar_display_url = serializers.SerializerMethodField()
def get_avatar_display_url(self, obj):
"""智能返回头像URL优先本地图片"""
request = self.context.get('request')
avatar_url = obj.get_avatar_url()
if avatar_url and request:
if obj.avatar:
return request.build_absolute_uri(avatar_url)
else:
return avatar_url
return avatar_url
```
### 4. 依赖安装
添加了必需的依赖:
```txt
# requirements.txt
Pillow==11.1.0 # Django ImageField所需
```
## 使用方式
### 方式一:本地图片上传(推荐)
```python
# 创建创作者并上传本地图片
creator = CreatorProfile.objects.create(name="张三")
# 通过Django admin、表单或API上传图片
with open('avatar.jpg', 'rb') as f:
creator.avatar.save('zhang_san.jpg', f)
print(creator.get_avatar_url()) # 返回: /media/avatars/zhang_san.jpg
```
### 方式二外部URL向后兼容
```python
# 使用外部URL
creator = CreatorProfile.objects.create(
name="李四",
avatar_url="https://example.com/avatar.jpg"
)
print(creator.get_avatar_url()) # 返回: https://example.com/avatar.jpg
```
### 方式三:混合使用
```python
# 既有外部URL又有本地图片时优先使用本地图片
creator = CreatorProfile.objects.create(
name="王五",
avatar_url="https://example.com/backup.jpg" # 备用URL
)
# 后来上传了本地图片
creator.avatar.save('wang_wu.jpg', image_file)
print(creator.get_avatar_url()) # 返回本地图片URL不是外部URL
```
## API使用
### 获取头像数据
```javascript
// 前端JavaScript
fetch('/api/daren_detail/creators/1/')
.then(response => response.json())
.then(data => {
// 使用avatar_display_url字段自动选择最佳URL
if (data.avatar_display_url) {
document.getElementById('avatar').src = data.avatar_display_url;
}
});
```
### 上传头像
```javascript
// 上传本地图片
function uploadAvatar(file, creatorId) {
const formData = new FormData();
formData.append('avatar', file);
fetch(`/api/daren_detail/creators/${creatorId}/`, {
method: 'PATCH',
body: formData
})
.then(response => response.json())
.then(data => {
console.log('上传成功:', data.avatar_display_url);
});
}
```
## 文件结构
```
项目根目录/
├── media/ # 媒体文件目录
│ └── avatars/ # 头像存储目录
│ ├── zhang_san.jpg
│ └── wang_wu.jpg
├── apps/daren_detail/
│ ├── models.py # 包含avatar字段
│ └── serializers.py # 新增的序列化器
└── daren/
├── settings.py # 媒体文件配置
└── urls.py # 媒体文件URL配置
```
## 数据库更改
已经执行的迁移:
```bash
python manage.py makemigrations daren_detail
python manage.py migrate
```
新增数据库字段:
- `avatar` (varchar): 存储本地图片路径,如 `avatars/zhang_san.jpg`
## 优势
1. **向后兼容**:原有的 `avatar_url` 字段继续工作
2. **智能选择**:优先使用本地图片,降低外部依赖
3. **性能优化**:本地图片加载更快
4. **数据控制**:头像文件存储在自己的服务器上
5. **灵活性**:支持两种方式同时存在
## 注意事项
1. **生产环境**建议使用CDN或对象存储服务如AWS S3
2. **图片优化**:可以添加图片压缩和缩放功能
3. **存储限制**:注意设置合理的文件大小限制
4. **安全性**:验证上传文件类型,防止恶意文件上传
## 下一步建议
1. 在Django admin中为avatar字段添加图片预览
2. 实现图片自动压缩和多尺寸生成
3. 添加图片格式验证和文件大小限制
4. 考虑使用django-imagekit进行高级图片处理

View File

@ -1,3 +1,83 @@
from django.contrib import admin
from django.utils.html import format_html
from django.utils.safestring import mark_safe
from .models import CreatorProfile
# Register your models here.
@admin.register(CreatorProfile)
class CreatorProfileAdmin(admin.ModelAdmin):
list_display = (
'avatar_thumbnail', 'name', 'category', 'e_commerce_level', 'exposure_level',
'followers', 'gmv', 'items_sold', 'avg_video_views', 'pricing', 'collab_count',
'mcn', 'location', 'create_time'
)
list_filter = (
'category', 'e_commerce_level', 'exposure_level', 'mcn', 'location',
'e_commerce_platforms', 'create_time', 'update_time'
)
search_fields = (
'name', 'email', 'instagram', 'tiktok_link', 'location', 'mcn',
'latest_collab', 'live_schedule', 'pricing_package'
)
readonly_fields = ('create_time', 'update_time', 'avatar_preview')
def avatar_thumbnail(self, obj):
"""在列表中显示头像缩略图"""
avatar_url = obj.get_avatar_url()
if avatar_url:
return format_html('<img src="{}" width="40" height="40" style="border-radius: 50%; object-fit: cover;" />', avatar_url)
return "无头像"
avatar_thumbnail.short_description = "头像"
def avatar_preview(self, obj):
"""在详情页显示头像预览"""
avatar_url = obj.get_avatar_url()
if avatar_url:
return format_html('<img src="{}" width="150" height="150" style="border-radius: 10px; object-fit: cover;" />', avatar_url)
return "无头像"
avatar_preview.short_description = "头像预览"
fieldsets = (
('基本信息', {
'fields': ('name', 'avatar', 'avatar_url', 'avatar_preview')
}),
('联系方式', {
'fields': ('email', 'instagram', 'tiktok_link', 'location', 'live_schedule')
}),
('分类等级', {
'fields': ('category', 'e_commerce_level', 'exposure_level')
}),
('数据指标', {
'fields': ('followers', 'gmv', 'items_sold', 'avg_video_views')
}),
('定价信息', {
'fields': ('pricing', 'pricing_package')
}),
('合作信息', {
'fields': ('collab_count', 'latest_collab', 'mcn')
}),
('电商平台', {
'fields': ('e_commerce_platforms',)
}),
('分析数据', {
'fields': ('gmv_by_channel', 'gmv_by_category'),
'classes': ('collapse',)
}),
('时间信息', {
'fields': ('create_time', 'update_time'),
'classes': ('collapse',)
}),
)
list_per_page = 20
ordering = ('-create_time',)
# 添加批量操作
actions = ['export_selected_creators']
def export_selected_creators(self, request, queryset):
"""批量导出选中的达人信息"""
# 这里可以添加导出逻辑
self.message_user(request, f"已选择 {queryset.count()} 个达人进行导出")
export_selected_creators.short_description = "导出选中的达人信息"

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,18 @@
# Generated by Django 5.1.5 on 2025-05-23 09:03
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('daren_detail', '0002_remove_creatorprofile_pricing_max_and_more'),
]
operations = [
migrations.AlterField(
model_name='privatecreatorrelation',
name='status',
field=models.CharField(choices=[('active', '活跃'), ('archived', '已归档'), ('favorite', '收藏'), ('public_removed', '公有库已移除')], default='active', max_length=20, verbose_name='状态'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 5.1.5 on 2025-05-23 09:14
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('daren_detail', '0003_alter_privatecreatorrelation_status'),
]
operations = [
migrations.AddField(
model_name='creatorprofile',
name='avatar',
field=models.ImageField(blank=True, null=True, upload_to='avatars/', verbose_name='头像图片'),
),
]

View File

@ -113,6 +113,8 @@ class CreatorProfile(models.Model):
"""达人信息模型,用于筛选功能"""
# 基本信息
name = models.CharField(max_length=255, verbose_name="达人名称")
# 修改为支持本地图片上传同时保持URL兼容性
avatar = models.ImageField(upload_to='avatars/', blank=True, null=True, verbose_name="头像图片")
avatar_url = models.TextField(blank=True, null=True, verbose_name="头像URL")
# 新增联系方式
@ -211,6 +213,14 @@ class CreatorProfile(models.Model):
create_time = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
update_time = models.DateTimeField(auto_now=True, verbose_name="更新时间")
def get_avatar_url(self):
"""获取头像URL优先返回本地图片其次返回外部URL"""
if self.avatar:
return self.avatar.url
elif self.avatar_url:
return self.avatar_url
return None
class Meta:
verbose_name = "达人信息"
verbose_name_plural = verbose_name
@ -466,7 +476,8 @@ class PrivateCreatorRelation(models.Model):
choices=[
("active", "活跃"),
("archived", "已归档"),
("favorite", "收藏")
("favorite", "收藏"),
("public_removed", "公有库已移除")
])
# 时间戳

View File

@ -0,0 +1,61 @@
from rest_framework import serializers
from .models import CreatorProfile
class CreatorProfileSerializer(serializers.ModelSerializer):
"""创作者资料序列化器,包含头像处理"""
avatar_display_url = serializers.SerializerMethodField()
class Meta:
model = CreatorProfile
fields = [
'id', 'name', 'avatar', 'avatar_url', 'avatar_display_url',
'email', 'instagram', 'tiktok_link', 'location', 'live_schedule',
'category', 'e_commerce_level', 'exposure_level', 'followers',
'gmv', 'items_sold', 'avg_video_views', 'pricing', 'pricing_package',
'collab_count', 'latest_collab', 'e_commerce_platforms',
'gmv_by_channel', 'gmv_by_category', 'mcn',
'create_time', 'update_time'
]
extra_kwargs = {
'avatar': {'write_only': False}, # 允许读写
'avatar_url': {'write_only': False} # 允许读写
}
def get_avatar_display_url(self, obj):
"""获取头像显示URL优先使用本地图片"""
request = self.context.get('request')
avatar_url = obj.get_avatar_url()
if avatar_url and request:
# 如果是本地图片返回完整的URL
if obj.avatar:
return request.build_absolute_uri(avatar_url)
# 如果是外部URL直接返回
else:
return avatar_url
return avatar_url
class CreatorProfileListSerializer(serializers.ModelSerializer):
"""创作者资料列表序列化器,用于列表显示,字段较少"""
avatar_display_url = serializers.SerializerMethodField()
class Meta:
model = CreatorProfile
fields = [
'id', 'name', 'avatar_display_url', 'category',
'exposure_level', 'followers', 'gmv', 'mcn'
]
def get_avatar_display_url(self, obj):
"""获取头像显示URL"""
request = self.context.get('request')
avatar_url = obj.get_avatar_url()
if avatar_url and request:
if obj.avatar:
return request.build_absolute_uri(avatar_url)
else:
return avatar_url
return avatar_url

View File

@ -1,74 +1,75 @@
from django.urls import path, include
from . import views
from django.http import HttpResponse
# 添加一个简单的index视图函数
# def index(request):
# return HttpResponse("Welcome to TikTok Videos Analysis API")
urlpatterns = [
# path('', index, name='index'),
# # TikTok API路由 - 旧路径保持不变以保证兼容性
# path('tiktok/user-videos/', views.get_tiktok_user_videos, name='get-tiktok-user-videos'),
# path('tiktok/fetch_videos/', views.fetch_tiktok_videos, name='fetch_tiktok_videos'),
# path('tiktok/delete_user/', views.delete_tiktok_user, name='delete_tiktok_user'),
# 新的API路由结构 - 只保留TikTok API
# path('api/tiktok/', include('app.api.tiktok.urls')), # TikTok API
# 添加Creator相关API
path('creators/filter/', views.filter_creators, name='filter_creators'),
path('creators/add/', views.add_creator, name='add_creator'),
path('creators/<int:creator_id>/', views.get_creator_detail, name='get_creator_detail'),
path('creators/update_detail/', views.update_creator_detail, name='update_creator_detail'),
path('creators/<int:creator_id>/campaigns/', views.get_creator_brand_campaigns, name='get_creator_brand_campaigns'),
# 添加Campaign相关API
path('campaigns/', views.get_campaigns, name='get_campaigns'),
path('campaigns/add/', views.add_to_campaign, name='add_to_campaign'),
# 新增的指标相关API
path('creators/<int:creator_id>/metrics/', views.get_creator_metrics, name='get_creator_metrics'),
# 获取创作者的所有指标数据
path('creators/metrics/update/', views.update_creator_metrics, name='update_creator_metrics'), # 更新创作者的指标数据
# 添加粉丝统计和趋势数据相关的路由
path('creator/<int:creator_id>/followers/', views.get_creator_followers_metrics, name='get_creator_followers'),
path('creator/followers/', views.get_creator_followers_metrics, name='get_creator_followers_query'),
# 获取创作者趋势数据
path('creator/<int:creator_id>/trends/', views.get_creator_trends, name='get_creator_trends'),
path('creator/trends/', views.get_creator_trends, name='get_creator_trends_query'),
# 更新创作者粉丝统计数据
path('creator/followers/update/', views.update_creator_followers, name='update_creator_followers'),
# 更新创作者趋势数据
path('creator/trend/update/', views.update_creator_trend, name='update_creator_trend'),
# 添加创作者视频相关的路由
path('creator/<int:creator_id>/videos/', views.get_creator_videos, name='get_creator_videos'),
path('creator/videos/', views.get_creator_videos, name='get_creator_videos_query'),
# 添加创作者视频
path('creator/video/add/', views.add_creator_video, name='add_creator_video'),
# 公有达人和私有达人API
path('public/creators/', views.get_public_creators, name='get_public_creators'),
path('public/creators/filter/', views.filter_public_creators, name='filter_public_creators'),
path('public/creators/add/', views.add_to_public_pool, name='add_to_public_pool'),
# 私有达人库
path('private/pools/', views.get_private_pools, name='get_private_pools'),
path('private/pools/create/', views.create_private_pool, name='create_private_pool'),
path('private/pools/creators/<int:pool_id>/', views.get_private_pool_creators, name='get_private_pool_creators'),
path('private/pools/creators/', views.get_private_pool_creators, name='get_private_pool_creators_no_id'),
path('private/pools/creators/add/', views.add_creator_to_private_pool, name='add_creator_to_private_pool'),
path('private/pools/creators/update/', views.update_creator_in_private_pool, name='update_creator_in_private_pool'),
path('private/pools/creators/remove/', views.remove_creator_from_private_pool, name='remove_creator_from_private_pool'),
path('private/pools/creators/filter/', views.filter_private_pool_creators, name='filter_private_pool_creators'),
]
from django.urls import path, include
from . import views
from django.http import HttpResponse
# 添加一个简单的index视图函数
# def index(request):
# return HttpResponse("Welcome to TikTok Videos Analysis API")
urlpatterns = [
# path('', index, name='index'),
# # TikTok API路由 - 旧路径保持不变以保证兼容性
# path('tiktok/user-videos/', views.get_tiktok_user_videos, name='get-tiktok-user-videos'),
# path('tiktok/fetch_videos/', views.fetch_tiktok_videos, name='fetch_tiktok_videos'),
# path('tiktok/delete_user/', views.delete_tiktok_user, name='delete_tiktok_user'),
# 新的API路由结构 - 只保留TikTok API
# path('api/tiktok/', include('app.api.tiktok.urls')), # TikTok API
# 添加Creator相关API
path('creators/filter/', views.filter_creators, name='filter_creators'),
path('creators/add/', views.add_creator, name='add_creator'),
path('creators/<int:creator_id>/', views.get_creator_detail, name='get_creator_detail'),
path('creators/update_detail/', views.update_creator_detail, name='update_creator_detail'),
path('creators/<int:creator_id>/campaigns/', views.get_creator_brand_campaigns, name='get_creator_brand_campaigns'),
# 添加Campaign相关API
path('campaigns/', views.get_campaigns, name='get_campaigns'),
path('campaigns/add/', views.add_to_campaign, name='add_to_campaign'),
# 新增的指标相关API
path('creators/<int:creator_id>/metrics/', views.get_creator_metrics, name='get_creator_metrics'),
# 获取创作者的所有指标数据
path('creators/metrics/update/', views.update_creator_metrics, name='update_creator_metrics'), # 更新创作者的指标数据
# 添加粉丝统计和趋势数据相关的路由
path('creator/<int:creator_id>/followers/', views.get_creator_followers_metrics, name='get_creator_followers'),
path('creator/followers/', views.get_creator_followers_metrics, name='get_creator_followers_query'),
# 获取创作者趋势数据
path('creator/<int:creator_id>/trends/', views.get_creator_trends, name='get_creator_trends'),
path('creator/trends/', views.get_creator_trends, name='get_creator_trends_query'),
# 更新创作者粉丝统计数据
path('creator/followers/update/', views.update_creator_followers, name='update_creator_followers'),
# 更新创作者趋势数据
path('creator/trend/update/', views.update_creator_trend, name='update_creator_trend'),
# 添加创作者视频相关的路由
path('creator/<int:creator_id>/videos/', views.get_creator_videos, name='get_creator_videos'),
path('creator/videos/', views.get_creator_videos, name='get_creator_videos_query'),
# 添加创作者视频
path('creator/video/add/', views.add_creator_video, name='add_creator_video'),
# 公有达人和私有达人API
path('public/creators/', views.get_public_creators, name='get_public_creators'),
path('public/creators/filter/', views.filter_public_creators, name='filter_public_creators'),
path('public/creators/add/', views.add_to_public_pool, name='add_to_public_pool'),
path('public/creators/remove/', views.remove_from_public_pool, name='remove_from_public_pool'),
# 私有达人库
path('private/pools/', views.get_private_pools, name='get_private_pools'),
path('private/pools/create/', views.create_private_pool, name='create_private_pool'),
path('private/pools/creators/<int:pool_id>/', views.get_private_pool_creators, name='get_private_pool_creators'),
path('private/pools/creators/', views.get_private_pool_creators, name='get_private_pool_creators_no_id'),
path('private/pools/creators/add/', views.add_creator_to_private_pool, name='add_creator_to_private_pool'),
path('private/pools/creators/update/', views.update_creator_in_private_pool, name='update_creator_in_private_pool'),
path('private/pools/creators/remove/', views.remove_creator_from_private_pool, name='remove_creator_from_private_pool'),
path('private/pools/creators/filter/', views.filter_private_pool_creators, name='filter_private_pool_creators'),
]

View File

@ -1312,19 +1312,25 @@ def get_creator_followers_metrics(request, creator_id=None):
}
else:
# 构建粉丝统计数据
# 处理locations数据对每个值保留两位小数
processed_locations = {}
if follower_metrics.location_data:
for location, value in follower_metrics.location_data.items():
processed_locations[location] = round(float(value), 2)
follower_data = {
'gender': {
'female': follower_metrics.female_percentage,
'male': follower_metrics.male_percentage
'female': round(follower_metrics.female_percentage, 2),
'male': round(follower_metrics.male_percentage, 2)
},
'age': {
'18-24': follower_metrics.age_18_24_percentage,
'25-34': follower_metrics.age_25_34_percentage,
'35-44': follower_metrics.age_35_44_percentage,
'45-54': follower_metrics.age_45_54_percentage,
'55+': follower_metrics.age_55_plus_percentage
'18-24': round(follower_metrics.age_18_24_percentage, 2),
'25-34': round(follower_metrics.age_25_34_percentage, 2),
'35-44': round(follower_metrics.age_35_44_percentage, 2),
'45-54': round(follower_metrics.age_45_54_percentage, 2),
'55+': round(follower_metrics.age_55_plus_percentage, 2)
},
'locations': follower_metrics.location_data,
'locations': processed_locations,
'date_range': {
'start_date': follower_metrics.start_date.strftime('%Y-%m-%d'),
'end_date': follower_metrics.end_date.strftime('%Y-%m-%d')
@ -1441,12 +1447,12 @@ def get_creator_trends(request, creator_id=None):
views_value = int(base_views + random.uniform(-base_views * 0.2, base_views * 0.4))
engagement_value = base_engagement + random.uniform(-base_engagement * 0.1, base_engagement * 0.15)
# 确保值不小于0
trend_data['gmv'].append(max(0, gmv_value))
# 确保值不小于0,并对浮点数保留两位小数
trend_data['gmv'].append(round(max(0, gmv_value), 2))
trend_data['items_sold'].append(max(0, items_value))
trend_data['followers'].append(max(0, followers_value))
trend_data['video_views'].append(max(0, views_value))
trend_data['engagement_rate'].append(max(0, engagement_value))
trend_data['engagement_rate'].append(round(max(0, engagement_value), 2))
# 更新基准值,使数据有一定的连续性
base_gmv = gmv_value
@ -1471,11 +1477,11 @@ def get_creator_trends(request, creator_id=None):
}
for trend in trends:
trend_data['gmv'].append(trend.gmv)
trend_data['gmv'].append(round(trend.gmv, 2))
trend_data['items_sold'].append(trend.items_sold)
trend_data['followers'].append(trend.followers_count)
trend_data['video_views'].append(trend.video_views)
trend_data['engagement_rate'].append(trend.engagement_rate)
trend_data['engagement_rate'].append(round(trend.engagement_rate, 2))
trend_data['dates'].append(trend.date.strftime('%Y-%m-%d'))
trend_data['date_range'] = {
@ -2486,7 +2492,9 @@ def get_private_pool_creators(request, pool_id=None):
"notes": relation.notes,
"status": relation.status,
"added_from_public": relation.added_from_public,
"added_at": relation.created_at.strftime('%Y-%m-%d')
"added_at": relation.created_at.strftime('%Y-%m-%d'),
"is_public_removed": relation.status == 'public_removed', # 添加公有库移除标识
"status_note": "该达人已从公有库中移除" if relation.status == 'public_removed' else None # 状态说明
}
creator_list.append(formatted_creator)
@ -3234,12 +3242,13 @@ def filter_private_pool_creators(request):
"gmv": gmv_formatted,
"avg_video_views": avg_views_formatted,
"pricing": pricing_range, # 使用格式化后的价格区间
"pricing_package": creator.pricing_package,
"collab_count": creator.collab_count,
"notes": relation.notes,
"status": relation.status,
"added_from_public": relation.added_from_public,
"added_at": relation.created_at.strftime('%Y-%m-%d')
"added_at": relation.created_at.strftime('%Y-%m-%d'),
"is_public_removed": relation.status == 'public_removed', # 添加公有库移除标识
"status_note": "该达人已从公有库中移除" if relation.status == 'public_removed' else None # 状态说明
}
creator_list.append(formatted_creator)
@ -3284,6 +3293,89 @@ def filter_private_pool_creators(request):
}, json_dumps_params={'ensure_ascii': False})
@api_view(['POST'])
@authentication_classes([CustomTokenAuthentication])
@csrf_exempt
@require_http_methods(["POST"])
def remove_from_public_pool(request):
"""从公有达人库中移除达人,同时更新私有库中相关记录的状态"""
try:
from .models import PublicCreatorPool, PrivateCreatorRelation, CreatorProfile
import json
data = json.loads(request.body)
# 获取必要参数
creator_id = data.get('creator_id')
public_id = data.get('public_id') # 也可以通过public_id删除
if not creator_id and not public_id:
return JsonResponse({
'code': 400,
'message': '缺少必要参数: creator_id 或 public_id',
'data': None
}, json_dumps_params={'ensure_ascii': False})
# 查询公有库记录
try:
if public_id:
public_creator = PublicCreatorPool.objects.get(id=public_id)
creator = public_creator.creator
else:
creator = CreatorProfile.objects.get(id=creator_id)
public_creator = PublicCreatorPool.objects.get(creator=creator)
except (PublicCreatorPool.DoesNotExist, CreatorProfile.DoesNotExist):
return JsonResponse({
'code': 404,
'message': '找不到指定的公有库达人记录',
'data': None
}, json_dumps_params={'ensure_ascii': False})
# 查找所有私有库中引用此达人且标记为从公有库添加的记录
private_relations = PrivateCreatorRelation.objects.filter(
creator=creator,
added_from_public=True,
status='active' # 只更新活跃状态的记录
)
# 更新私有库中相关记录的状态为"已失效"
updated_private_count = 0
if private_relations.exists():
# 为了兼容性可以选择添加新的状态或使用现有的archived状态
# 这里我们添加一个新的状态值来明确表示"公有库已移除"
updated_private_count = private_relations.update(
status='public_removed' # 新增状态:公有库已移除
)
# 删除公有库记录
public_creator.delete()
return JsonResponse({
'code': 200,
'message': '成功从公有库移除达人',
'data': {
'creator': {
'id': creator.id,
'name': creator.name
},
'removed_from_public': True,
'updated_private_relations': updated_private_count,
'note': f'已更新 {updated_private_count} 个私有库中的相关记录状态'
}
}, json_dumps_params={'ensure_ascii': False})
except Exception as e:
logger.error(f"从公有库移除达人失败: {e}")
import traceback
logger.error(f"详细错误: {traceback.format_exc()}")
return JsonResponse({
'code': 500,
'message': f'从公有库移除达人失败: {str(e)}',
'data': None
}, json_dumps_params={'ensure_ascii': False})

View File

@ -0,0 +1,18 @@
# Generated by Django 5.1.5 on 2025-05-23 09:32
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('user', '0004_user_is_active_user_is_staff'),
]
operations = [
migrations.AddField(
model_name='user',
name='is_superuser',
field=models.BooleanField(default=False, verbose_name='超级用户状态'),
),
]

View File

@ -14,6 +14,17 @@ class UserManager(BaseUserManager):
user.save(using=self._db)
return user
def create_superuser(self, email, password=None, **extra_fields):
extra_fields.setdefault('is_staff', True)
extra_fields.setdefault('is_superuser', True)
if extra_fields.get('is_staff') is not True:
raise ValueError('超级用户必须设置is_staff=True')
if extra_fields.get('is_superuser') is not True:
raise ValueError('超级用户必须设置is_superuser=True')
return self.create_user(email, password, **extra_fields)
class User(AbstractBaseUser):
"""用户模型,用于登录和账户管理"""
email = models.EmailField(max_length=255, unique=True, verbose_name="电子邮箱")
@ -24,6 +35,7 @@ class User(AbstractBaseUser):
last_login = models.DateTimeField(blank=True, null=True, verbose_name="最近登录时间")
is_active = models.BooleanField(default=True)
is_staff = models.BooleanField(default=False)
is_superuser = models.BooleanField(default=False, verbose_name="超级用户状态")
# 时间戳
created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
@ -46,6 +58,14 @@ class User(AbstractBaseUser):
def is_authenticated(self):
return True
def has_perm(self, perm, obj=None):
"""用户是否有特定权限"""
return self.is_superuser
def has_module_perms(self, app_label):
"""用户是否有访问特定app的权限"""
return self.is_superuser
class UserToken(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='tokens')
token = models.CharField(max_length=40, unique=True)

View File

@ -207,6 +207,10 @@ LOGGING = {
# 自定义用户模型
AUTH_USER_MODEL = 'user.User'
# 媒体文件配置
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
# REST Framework 设置
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [], # 默认不需要认证

View File

@ -4,6 +4,8 @@ from rest_framework_simplejwt.views import (
TokenObtainPairView,
TokenRefreshView,
)
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [
path('admin/', admin.site.urls),
@ -16,3 +18,7 @@ urlpatterns = [
path('api/template/', include('apps.template.urls')),
path('api/', include('apps.brands.urls')),
]
# 在开发环境中提供媒体文件服务
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

153
example_usage.py Normal file
View File

@ -0,0 +1,153 @@
"""
CreatorProfile头像字段使用示例
演示如何在Django项目中使用avatar字段来处理本地图片和外部URL
"""
from django.core.files.uploadedfile import SimpleUploadedFile
from apps.daren_detail.models import CreatorProfile
from django.conf import settings
import os
def create_creator_with_local_avatar():
"""创建一个使用本地图片作为头像的创作者"""
# 方式1通过Django admin或表单上传图片
# 用户上传图片后Django会自动保存到 MEDIA_ROOT/avatars/ 目录
creator = CreatorProfile.objects.create(
name="张三",
category="Beauty & Personal Care",
followers=10000
)
# 假设有一个图片文件
# with open('path/to/image.jpg', 'rb') as f:
# creator.avatar.save('zhang_san.jpg', f)
print(f"创作者: {creator.name}")
print(f"头像URL: {creator.get_avatar_url()}")
return creator
def create_creator_with_external_url():
"""创建一个使用外部URL作为头像的创作者"""
creator = CreatorProfile.objects.create(
name="李四",
avatar_url="https://example.com/avatar.jpg",
category="Fashion Accessories",
followers=20000
)
print(f"创作者: {creator.name}")
print(f"头像URL: {creator.get_avatar_url()}")
return creator
def create_creator_with_both():
"""创建一个既有本地图片又有外部URL的创作者优先使用本地图片"""
creator = CreatorProfile.objects.create(
name="王五",
avatar_url="https://example.com/backup-avatar.jpg",
category="Sports & Outdoor",
followers=30000
)
# 如果后来上传了本地图片,会优先使用本地图片
# creator.avatar.save('wang_wu.jpg', image_file)
print(f"创作者: {creator.name}")
print(f"头像URL: {creator.get_avatar_url()}")
return creator
# API使用示例
def api_response_example():
"""API响应示例"""
# 在视图中使用序列化器
from apps.daren_detail.serializers import CreatorProfileSerializer
from django.http import HttpRequest
creator = CreatorProfile.objects.first()
# 创建一个模拟的request对象
request = HttpRequest()
request.META['HTTP_HOST'] = 'localhost:8000'
request.META['wsgi.url_scheme'] = 'http'
serializer = CreatorProfileSerializer(creator, context={'request': request})
data = serializer.data
print("API响应示例:")
print(f"名称: {data['name']}")
print(f"本地头像字段: {data.get('avatar')}")
print(f"外部头像URL: {data.get('avatar_url')}")
print(f"实际显示URL: {data.get('avatar_display_url')}")
# 前端使用示例JavaScript
frontend_example = """
// 前端JavaScript使用示例
// 获取创作者数据
fetch('/api/daren_detail/creators/1/')
.then(response => response.json())
.then(data => {
// 显示头像
const avatarImg = document.getElementById('avatar');
// 使用avatar_display_url字段它会自动选择合适的URL
if (data.avatar_display_url) {
avatarImg.src = data.avatar_display_url;
avatarImg.style.display = 'block';
} else {
// 如果没有头像显示默认头像
avatarImg.src = '/static/images/default-avatar.png';
}
// 或者可以检查具体的字段类型
if (data.avatar) {
// 这是本地上传的图片
console.log('使用本地头像:', data.avatar);
} else if (data.avatar_url) {
// 这是外部URL
console.log('使用外部头像:', data.avatar_url);
}
});
// 上传头像示例
function uploadAvatar(file, creatorId) {
const formData = new FormData();
formData.append('avatar', file);
fetch(`/api/daren_detail/creators/${creatorId}/`, {
method: 'PATCH',
body: formData,
headers: {
'X-CSRFToken': getCookie('csrftoken') // CSRF令牌
}
})
.then(response => response.json())
.then(data => {
console.log('头像上传成功:', data.avatar_display_url);
// 更新页面上的头像显示
document.getElementById('avatar').src = data.avatar_display_url;
})
.catch(error => {
console.error('头像上传失败:', error);
});
}
"""
if __name__ == "__main__":
print("CreatorProfile头像字段使用示例")
print("="*50)
print()
print("1. 支持本地图片上传到 media/avatars/ 目录")
print("2. 支持外部URL链接")
print("3. get_avatar_url() 方法优先返回本地图片URL")
print("4. 序列化器提供avatar_display_url字段用于前端显示")
print()
print("前端JavaScript示例:")
print(frontend_example)

Binary file not shown.

117
test_avatar_display.py Normal file
View File

@ -0,0 +1,117 @@
#!/usr/bin/env python
"""
测试达人头像显示功能
演示如何使用本地图片和外部URL
"""
import os
import django
# 设置Django环境
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'daren.settings')
django.setup()
from apps.daren_detail.models import CreatorProfile
from django.core.files import File
from django.core.files.uploadedfile import SimpleUploadedFile
def test_avatar_display():
"""测试头像显示功能"""
print("=== 达人头像显示功能测试 ===\n")
# 1. 查询现有达人
creators = CreatorProfile.objects.all()[:3]
print("1. 现有达人头像状态:")
for creator in creators:
avatar_url = creator.get_avatar_url()
has_local = bool(creator.avatar)
has_external = bool(creator.avatar_url)
print(f" - {creator.name}:")
print(f" 本地图片: {'' if has_local else ''}")
print(f" 外部URL: {'' if has_external else ''}")
print(f" 显示URL: {avatar_url or ''}")
print()
# 2. 演示URL访问方式
print("2. 头像URL访问示例")
for creator in creators:
avatar_url = creator.get_avatar_url()
if avatar_url:
if creator.avatar:
print(f" 本地图片: http://localhost:8000{avatar_url}")
else:
print(f" 外部URL: {avatar_url}")
else:
print(f" {creator.name}: 无头像")
print()
# 3. 创建测试数据示例
print("3. 创建测试达人示例:")
# 示例1仅外部URL
creator1, created = CreatorProfile.objects.get_or_create(
name="测试达人A",
defaults={
'avatar_url': 'https://example.com/avatar1.jpg',
'category': 'Beauty & Personal Care',
'followers': 1000
}
)
print(f" - {creator1.name}: {creator1.get_avatar_url()}")
# 示例2仅本地图片如果存在的话
existing_avatar = CreatorProfile.objects.filter(avatar__isnull=False).first()
if existing_avatar:
print(f" - {existing_avatar.name}: {existing_avatar.get_avatar_url()}")
print("\n=== 前端使用示例代码 ===")
print("""
// JavaScript: 获取并显示头像
fetch('/api/daren_detail/creators/')
.then(response => response.json())
.then(data => {
data.results.forEach(creator => {
if (creator.avatar_display_url) {
console.log(`${creator.name}: ${creator.avatar_display_url}`);
// 创建图片元素
const img = document.createElement('img');
img.src = creator.avatar_display_url;
img.alt = `${creator.name}的头像`;
img.className = 'creator-avatar';
// 添加到页面
document.getElementById('creators-list').appendChild(img);
}
});
});
""")
print("\n=== HTML模板使用示例 ===")
print("""
<!-- Django模板中使用 -->
{% for creator in creators %}
<div class="creator-card">
<h3>{{ creator.name }}</h3>
{% if creator.get_avatar_url %}
<img src="{{ creator.get_avatar_url }}"
alt="{{ creator.name }}的头像"
class="avatar">
{% else %}
<div class="no-avatar">暂无头像</div>
{% endif %}
</div>
{% endfor %}
""")
if __name__ == "__main__":
try:
test_avatar_display()
except Exception as e:
print(f"测试出错: {e}")
print("请确保:")
print("1. 已运行 python manage.py migrate")
print("2. 数据库中有达人数据")
print("3. media/avatars/ 目录存在")