From b68062051a94a210396805837a5dd1d310c0aa87 Mon Sep 17 00:00:00 2001 From: jlj <3042504846@qq.com> Date: Fri, 23 May 2025 19:08:40 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E6=9D=83=E9=99=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AVATAR_SETUP_README.md | 211 ++++ apps/daren_detail/admin.py | 80 ++ apps/daren_detail/daren_detail.md | 1056 +++++++++++++++++ ...003_alter_privatecreatorrelation_status.py | 18 + .../migrations/0004_creatorprofile_avatar.py | 18 + apps/daren_detail/models.py | 13 +- apps/daren_detail/serializers.py | 61 + apps/daren_detail/urls.py | 149 +-- apps/daren_detail/views.py | 124 +- .../user/migrations/0005_user_is_superuser.py | 18 + apps/user/models.py | 20 + daren/settings.py | 4 + daren/urls.py | 6 + example_usage.py | 153 +++ requirements.txt | Bin 1646 -> 1754 bytes test_avatar_display.py | 117 ++ 16 files changed, 1957 insertions(+), 91 deletions(-) create mode 100644 AVATAR_SETUP_README.md create mode 100644 apps/daren_detail/daren_detail.md create mode 100644 apps/daren_detail/migrations/0003_alter_privatecreatorrelation_status.py create mode 100644 apps/daren_detail/migrations/0004_creatorprofile_avatar.py create mode 100644 apps/daren_detail/serializers.py create mode 100644 apps/user/migrations/0005_user_is_superuser.py create mode 100644 example_usage.py create mode 100644 test_avatar_display.py diff --git a/AVATAR_SETUP_README.md b/AVATAR_SETUP_README.md new file mode 100644 index 0000000..7cc5beb --- /dev/null +++ b/AVATAR_SETUP_README.md @@ -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进行高级图片处理 \ No newline at end of file diff --git a/apps/daren_detail/admin.py b/apps/daren_detail/admin.py index ea5d68b..fe20e1c 100644 --- a/apps/daren_detail/admin.py +++ b/apps/daren_detail/admin.py @@ -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('', avatar_url) + return "无头像" + avatar_thumbnail.short_description = "头像" + + def avatar_preview(self, obj): + """在详情页显示头像预览""" + avatar_url = obj.get_avatar_url() + if avatar_url: + return format_html('', 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 = "导出选中的达人信息" diff --git a/apps/daren_detail/daren_detail.md b/apps/daren_detail/daren_detail.md new file mode 100644 index 0000000..0e58860 --- /dev/null +++ b/apps/daren_detail/daren_detail.md @@ -0,0 +1,1056 @@ +# 达人详情模块 (daren_detail) API 接口文档 + +## 模块简介 + +`daren_detail` 模块是达人管理系统的核心模块,提供了完整的达人信息管理功能,包括: +- 达人筛选、详情查看、信息更新 +- 营销活动管理和达人合作详情 +- 多维度指标数据管理(协作指标、视频指标、直播指标、粉丝统计、趋势分析) +- 视频内容管理 +- 公有达人库和私有达人库管理 + +## 认证方式 + +所有接口都需要使用 `CustomTokenAuthentication` 进行身份验证,请在请求头中包含: +```http +Authorization: Token +``` + +## 1. 达人管理相关接口 + +### 1.1 添加/更新达人 +- **接口路径**: `POST /creators/add/` +- **功能**: 添加新达人或更新现有达人信息 +- **请求方法**: POST + +**请求体参数**: +```json +{ + "name": "达人姓名", // 必填 + "avatar_url": "头像URL", + "category": "Beauty & Personal Care", + "e_commerce_level": "L3", // 或数字3 + "exposure_level": "KOL-1", + "followers": 125000, + "gmv": "$534.1k", // 或数字534.1 + "items_sold": "2.5k", // 或数字2.5 + "avg_video_views": "85k", // 或数字85000 + "pricing": { + "individual": 200, // 或直接传数字 + "package": "套餐描述" + }, + "collab_count": 15, + "latest_collab": "2024-03-15", + "e_commerce_platforms": ["TikTok Shop", "Instagram Shop"], + "mcn": "MCN机构名称" +} +``` + +**响应格式**: +```json +{ + "code": 200, + "message": "达人信息已添加/更新", + "data": { + "created": true, // true为新建,false为更新 + "creator_id": 123 + } +} +``` + +### 1.2 获取达人详情 +- **接口路径**: `GET /creators/{creator_id}/` +- **功能**: 获取指定达人的详细信息 +- **请求方法**: GET + +**URL参数**: +- `creator_id`: 达人ID(必填) + +**响应格式**: +```json +{ + "code": 200, + "message": "获取成功", + "data": { + "creator": { + "id": 123, + "name": "达人姓名", + "avatar": "头像URL", + "email": "邮箱", + "social_accounts": { + "instagram": "@username", + "tiktok": "tiktok链接" + }, + "location": "地理位置", + "live_schedule": "直播时间安排" + }, + "metrics": { + "e_commerce_level": "L3", + "exposure_level": "KOL-1", + "followers": "125k", + "actual_followers": 125000, + "gmv": "$534k", + "actual_gmv": 534.1, + "items_sold": "2500", + "avg_video_views": "85k", + "actual_views": 85000, + "gpm": "$2.35", // 每千次观看收入 + "gmv_per_customer": "$213.64" // 每位客户GMV + }, + "business": { + "category": "Beauty & Personal Care", + "categories": ["Beauty & Personal Care"], + "mcn": "MCN机构", + "pricing": { + "price": "$200", + "package": "套餐描述" + }, + "collab_count": 15, + "latest_collab": "2024-03-15", + "e_commerce_platforms": ["TikTok Shop"] + }, + "analytics": { + "gmv_by_channel": {...}, // 按渠道分析 + "gmv_by_category": {...} // 按类别分析 + } + } +} +``` + +### 1.3 更新达人详细信息 +- **接口路径**: `POST /creators/update_detail/` +- **功能**: 更新达人的详细信息(联系方式、社交账号等) +- **请求方法**: POST + +**请求体参数**: +```json +{ + "creator_id": 123, // 必填 + "email": "新邮箱", + "instagram": "@new_username", + "tiktok_link": "新TikTok链接", + "location": "新地理位置", + "live_schedule": "新直播时间安排", + "mcn": "新MCN机构", + "gmv_by_channel": {...}, // 按渠道GMV分析数据 + "gmv_by_category": {...} // 按类别GMV分析数据 +} +``` + +**响应格式**: +```json +{ + "code": 200, + "message": "达人详细信息已更新", + "data": { + "creator_id": 123, + "name": "达人姓名" + } +} +``` + +### 1.4 获取达人品牌合作详情 +- **接口路径**: `GET /creators/{creator_id}/campaigns/` +- **功能**: 获取指定达人与各品牌的合作活动详情,支持分页 +- **请求方法**: GET + +**URL参数**: +- `creator_id`: 达人ID(必填) +- `page`: 页码,默认为1 +- `page_size`: 每页数量,默认为10 + +**响应格式**: +```json +{ + "code": 200, + "message": "获取成功", + "data": [ + { + "brand": { + "id": "U", + "name": "品牌名称", + "color": "#E74694" + }, + "pricing_detail": "$80", + "start_date": "05/31/2024", + "end_date": "05/29/2024", + "status": "completed", // pending, in_progress, completed, cancelled + "gmv_achieved": "$120", + "views_achieved": "650", + "video_link": "视频链接", + "campaign_id": 456, + "campaign_name": "活动名称" + } + ], + "pagination": { + "current_page": 1, + "total_pages": 5, + "total_count": 45, + "has_next": true, + "has_prev": false + }, + "creator": { + "id": 123, + "name": "达人姓名", + "avatar": "头像URL", + "category": "Beauty & Personal Care", + "exposure_level": "KOL-1" + } +} +``` + +## 2. 营销活动相关接口 + +### 2.1 获取营销活动列表 +- **接口路径**: `GET /campaigns/` +- **功能**: 获取所有活跃的营销活动列表 +- **请求方法**: GET + +**响应格式**: +```json +{ + "code": 200, + "message": "获取成功", + "data": [ + { + "id": 456, + "name": "品牌名 产品名", + "brand": "品牌名", + "product": "产品名" + } + ] +} +``` + +### 2.2 添加达人到营销活动 +- **接口路径**: `POST /campaigns/add/` +- **功能**: 将达人添加到指定营销活动中 +- **请求方法**: POST + +**请求体参数**: +```json +{ + "campaign_id": 456, // 必填 + "creator_ids": [123, 124, 125] // 达人ID列表,必填 +} +``` + +**响应格式**: +```json +{ + "code": 200, + "message": "成功添加达人到活动", + "data": { + "campaign": { + "id": "456", + "name": "活动名称" + }, + "added_creators": [ + { + "id": 123, + "name": "达人姓名" + } + ], + "stats": { + "added": 1, // 成功添加数量 + "skipped": 0, // 跳过数量(达人不存在) + "already_exists": 2 // 已存在数量 + } + } +} +``` + +## 3. 指标数据相关接口 + +### 3.1 获取创作者指标数据 +- **接口路径**: `GET /creators/{creator_id}/metrics/` +- **功能**: 获取创作者的协作指标、视频和直播指标数据 +- **请求方法**: GET + +**URL参数**: +- `creator_id`: 创作者ID(必填) +- `start_date`: 开始日期,格式:YYYY-MM-DD(可选) +- `end_date`: 结束日期,格式:YYYY-MM-DD(可选) + +**响应格式**: +```json +{ + "code": 200, + "message": "获取成功", + "data": { + "collaboration_metrics": { + "avg_commission_rate": "15.5%", + "products_count": 25, + "brand_collaborations": 8, + "product_price": "$50 - $200", + "date_range": "Mar 01, 2024 - Mar 31, 2024" + }, + "video": { + "gpm": "$2.35", + "videos_count": 12, + "avg_views": "85k", + "avg_engagement": "3.2%", + "avg_likes": 2500, + "date_range": "Mar 01, 2024 - Mar 31, 2024" + }, + "shoppable_video": { + "gpm": "$3.15", + "videos_count": 8, + "avg_views": "92k", + "avg_engagement": "4.1%", + "avg_likes": 3200, + "date_range": "Mar 01, 2024 - Mar 31, 2024" + }, + "live": { + "gpm": "$4.50", + "lives_count": 5, + "avg_views": "150k", + "avg_engagement": "5.8%", + "avg_likes": 8500, + "date_range": "Mar 01, 2024 - Mar 31, 2024" + }, + "shoppable_live": { + "gpm": "$6.20", + "lives_count": 3, + "avg_views": "180k", + "avg_engagement": "7.2%", + "avg_likes": 12000, + "date_range": "Mar 01, 2024 - Mar 31, 2024" + } + } +} +``` + +### 3.2 更新创作者指标数据 +- **接口路径**: `POST /creators/metrics/update/` +- **功能**: 更新创作者的指标数据 +- **请求方法**: POST + +**请求体参数**: +```json +{ + "creator_id": 123, // 必填 + "metrics_type": "collaboration", // 必填:collaboration, video, shoppable_video, live, shoppable_live + "metrics_data": { + "start_date": "2024-03-01", // 必填,格式:YYYY-MM-DD + "end_date": "2024-03-31", // 必填,格式:YYYY-MM-DD + // 根据metrics_type不同,以下参数有所不同: + + // 当metrics_type为"collaboration"时: + "avg_commission_rate": "15.5%", // 或数字15.5 + "products_count": 25, + "brand_collaborations": 8, + "product_price": "$50 - $200", // 格式:$最小值 - $最大值 + + // 当metrics_type为"video"或"shoppable_video"时: + "gpm": "$2.35", // 或数字2.35 + "videos_count": 12, + "avg_views": "85k", // 或数字85000 + "avg_engagement": "3.2%", // 或数字3.2 + "avg_likes": 2500, + + // 当metrics_type为"live"或"shoppable_live"时: + "gpm": "$4.50", + "lives_count": 5, + "avg_views": "150k", + "avg_engagement": "5.8%", + "avg_likes": 8500 + } +} +``` + +**响应格式**: +```json +{ + "code": 200, + "message": "协作指标数据已更新", // 根据metrics_type变化 + "data": { + "created": true, // true为新建,false为更新 + "metrics_id": 789 + } +} +``` + +### 3.3 获取创作者粉丝统计数据 +- **接口路径**: `GET /creator/{creator_id}/followers/` 或 `GET /creator/followers/?creator_id={creator_id}` +- **功能**: 获取创作者的粉丝性别、年龄、地域分布统计 +- **请求方法**: GET + +**URL参数**: +- `creator_id`: 创作者ID(必填) + +**响应格式**: +```json +{ + "code": 200, + "message": "获取成功", + "data": { + "gender": { + "female": 65.5, + "male": 34.5 + }, + "age": { + "18-24": 25.2, + "25-34": 35.8, + "35-44": 22.1, + "45-54": 12.3, + "55+": 4.6 + }, + "locations": { + "TX": 18.5, + "FL": 15.2, + "NY": 12.8, + "CA": 11.3, + "GE": 8.9 + }, + "date_range": { + "start_date": "2024-03-29", + "end_date": "2024-04-28" + } + } +} +``` + +### 3.4 更新创作者粉丝统计数据 +- **接口路径**: `POST /creator/followers/update/` +- **功能**: 更新创作者的粉丝统计数据 +- **请求方法**: POST + +**请求体参数**: +```json +{ + "creator_id": 123, // 必填 + "follower_data": { + "date_range": { + "start_date": "2024-03-29", // 必填 + "end_date": "2024-04-28" // 必填 + }, + "gender": { + "female": 65.5, + "male": 34.5 + }, + "age": { + "18-24": 25.2, + "25-34": 35.8, + "35-44": 22.1, + "45-54": 12.3, + "55+": 4.6 + }, + "locations": { + "TX": 18.5, + "FL": 15.2, + "NY": 12.8, + "CA": 11.3, + "GE": 8.9 + } + } +} +``` + +**响应格式**: +```json +{ + "code": 200, + "message": "粉丝统计数据已更新", + "data": { + "created": true, + "metrics_id": 890 + } +} +``` + +### 3.5 获取创作者趋势数据 +- **接口路径**: `GET /creator/{creator_id}/trends/` 或 `GET /creator/trends/?creator_id={creator_id}` +- **功能**: 获取创作者的GMV、销量、粉丝、观看量等趋势数据 +- **请求方法**: GET + +**URL参数**: +- `creator_id`: 创作者ID(必填) +- `start_date`: 开始日期,格式:YYYY-MM-DD(可选,默认最近30天) +- `end_date`: 结束日期,格式:YYYY-MM-DD(可选,默认今天) + +**响应格式**: +```json +{ + "code": 200, + "message": "获取成功", + "data": { + "gmv": [1250.5, 1380.2, 1156.8, ...], // 30天的GMV数据 + "items_sold": [850, 920, 780, ...], // 30天的销量数据 + "followers": [125000, 125150, 125300, ...], // 30天的粉丝数据 + "video_views": [85000, 92000, 78000, ...], // 30天的观看量数据 + "engagement_rate": [3.2, 3.5, 2.8, ...], // 30天的互动率数据 + "dates": ["2024-03-01", "2024-03-02", ...], // 对应的日期 + "date_range": { + "start_date": "2024-03-01", + "end_date": "2024-03-31" + } + } +} +``` + +### 3.6 更新创作者趋势数据 +- **接口路径**: `POST /creator/trend/update/` +- **功能**: 更新单日的创作者趋势数据 +- **请求方法**: POST + +**请求体参数**: +```json +{ + "creator_id": 123, // 必填 + "date": "2024-03-15", // 必填,格式:YYYY-MM-DD + "metrics": { + "gmv": 1250.5, + "items_sold": 850, + "followers": 125000, + "video_views": 85000, + "engagement_rate": 3.2 + } +} +``` + +**响应格式**: +```json +{ + "code": 200, + "message": "趋势数据已更新", + "data": { + "created": true, + "metrics_id": 901, + "date": "2024-03-15" + } +} +``` + +## 4. 视频管理相关接口 + +### 4.1 获取创作者视频列表 +- **接口路径**: `GET /creator/{creator_id}/videos/` 或 `GET /creator/videos/?creator_id={creator_id}` +- **功能**: 获取创作者的视频列表,分为普通视频和带产品视频 +- **请求方法**: GET + +**URL参数**: +- `creator_id`: 创作者ID(必填) +- `page`: 页码,默认为1 +- `page_size`: 每页数量,默认为6 + +**响应格式**: +```json +{ + "code": 200, + "message": "获取成功", + "data": { + "regular_videos": { + "videos": [ + { + "id": 1, + "title": "视频标题", + "thumbnail_url": "缩略图URL", + "badge": "red", // red, gold等 + "view_count": 2130, + "like_count": 20, + "release_date": "2025-03-31" + } + ], + "total": 15, + "page": 1, + "page_size": 6, + "total_pages": 3 + }, + "product_videos": { + "videos": [ + { + "id": 4, + "title": "带产品的视频标题", + "thumbnail_url": "缩略图URL", + "badge": "red", + "view_count": 2130, + "like_count": 20, + "release_date": "2025-03-31", + "has_product": true, + "product_name": "产品名称", + "product_url": "产品链接" + } + ], + "total": 8, + "page": 1, + "page_size": 6, + "total_pages": 2 + } + } +} +``` + +### 4.2 添加创作者视频 +- **接口路径**: `POST /creator/video/add/` +- **功能**: 为创作者添加新视频 +- **请求方法**: POST + +**请求体参数**: +```json +{ + "creator_id": 123, // 必填 + "title": "视频标题", // 必填 + "release_date": "2024-03-15", // 必填,格式:YYYY-MM-DD + "video_type": "regular", // 可选:regular, product,默认regular + "description": "视频描述", + "thumbnail_url": "缩略图URL", + "video_url": "视频URL", + "video_id": "vid_123456", // 可选,不提供会自动生成 + "badge": "red", // 可选:red, gold等,默认red + "view_count": 2130, + "like_count": 20, + "comment_count": 5, + "has_product": false, // 如果video_type为product,会自动设为true + "product_name": "产品名称", // 有产品时填写 + "product_url": "产品链接" // 有产品时填写 +} +``` + +**响应格式**: +```json +{ + "code": 200, + "message": "视频添加成功", + "data": { + "video_id": 456, + "created": true // true为新建,false为更新 + } +} +``` + +## 5. 公有达人库相关接口 + +### 5.1 获取公有达人库列表 +- **接口路径**: `GET /public/creators/` +- **功能**: 获取公有达人库中的达人列表 +- **请求方法**: GET + +**URL参数**: +- `page`: 页码,默认为1 +- `page_size`: 每页数量,默认为10 +- `category`: 分类过滤(可选) +- `keyword`: 关键词搜索(可选) + +**响应格式**: +```json +{ + "code": 200, + "message": "获取成功", + "data": [ + { + "public_id": 123, + "creator_id": 456, + "name": "达人姓名", + "avatar": "头像URL", + "category": "Beauty & Personal Care", + "e_commerce_level": "L3", + "exposure_level": "KOL-1", + "followers": "125k", + "gmv": "$534k", + "avg_video_views": "85k", + "pricing": "$200", + "pricing_package": "套餐描述", + "collab_count": 15, + "remark": "公有库备注", + "category_public": "公有库分类" + } + ], + "pagination": { + "current_page": 1, + "total_pages": 15, + "total_count": 150, + "has_next": true, + "has_prev": false + } +} +``` + +### 5.2 筛选公有达人库 +- **接口路径**: `POST /public/creators/filter/` +- **功能**: 根据过滤条件筛选公有达人库中的达人 +- **请求方法**: POST + +**URL参数**: +- `page`: 页码,默认为1 +- `page_size`: 每页数量,默认为10 + +**请求体参数**: +```json +{ + "filter": { + "category": ["Beauty & Personal Care"], // 多选 + "e_commerce_level": ["L1", "L2", "L3"], // 多选 + "exposure_level": ["KOL-1", "KOC-1"], // 多选 + "gmv_range": ["$0-$5k", "$5k-$25k"], // 多选 + "views_range": ["10k-100k"], // 单选 + "pricing": ["100-500"] // 单选 + } +} +``` + +**响应格式**: 同5.1 + +### 5.3 添加达人到公有库 +- **接口路径**: `POST /public/creators/add/` +- **功能**: 将达人添加到公有达人库 +- **请求方法**: POST + +**请求体参数**: +```json +{ + "creator_id": 456, // 必填 + "category": "分类", // 可选 + "remark": "备注信息" // 可选 +} +``` + +**响应格式**: +```json +{ + "code": 200, + "message": "成功添加达人到公有库", // 或"成功更新达人到公有库" + "data": { + "creator": { + "id": 456, + "name": "达人姓名" + }, + "public_pool": { + "id": 123, + "category": "分类", + "remark": "备注信息" + } + } +} +``` + +### 5.4 从公有库移除达人 +- **接口路径**: `POST /public/creators/remove/` +- **功能**: 从公有达人库中移除达人,同时更新私有库中相关记录的状态 +- **请求方法**: POST + +**请求体参数**: +```json +{ + "creator_id": 456, // 二选一 + "public_id": 123 // 二选一 +} +``` + +**响应格式**: +```json +{ + "code": 200, + "message": "成功从公有库移除达人", + "data": { + "creator": { + "id": 456, + "name": "达人姓名" + }, + "removed_from_public": true, + "updated_private_relations": 3, + "note": "已更新 3 个私有库中的相关记录状态" + } +} +``` + +## 6. 私有达人库相关接口 + +### 6.1 获取私有达人库列表 +- **接口路径**: `GET /private/pools/` +- **功能**: 获取当前用户的私有达人库列表 +- **请求方法**: GET + +**URL参数**: +- `user_id`: 用户ID(可选,只能查看自己的) + +**响应格式**: +```json +{ + "code": 200, + "message": "获取成功", + "data": [ + { + "id": 123, + "name": "我的收藏", + "description": "私有库描述", + "is_default": true, + "creator_count": 25, + "created_at": "2024-03-15" + } + ] +} +``` + +### 6.2 创建私有达人库 +- **接口路径**: `POST /private/pools/create/` +- **功能**: 创建新的私有达人库 +- **请求方法**: POST + +**请求体参数**: +```json +{ + "name": "私有库名称", // 必填 + "description": "私有库描述", // 可选 + "is_default": false // 可选,是否设为默认库 +} +``` + +**响应格式**: +```json +{ + "code": 200, + "message": "私有库创建成功", + "data": { + "id": 123, + "name": "私有库名称", + "is_default": false, + "creator_count": 0, + "created_at": "2024-03-15" + } +} +``` + +### 6.3 获取私有库中的达人 +- **接口路径**: `GET /private/pools/creators/{pool_id}/` 或 `GET /private/pools/creators/?pool_id={pool_id}` +- **功能**: 获取指定私有库中的达人列表 +- **请求方法**: GET + +**URL参数**: +- `pool_id`: 私有库ID(必填) +- `page`: 页码,默认为1 +- `page_size`: 每页数量,默认为10 +- `status`: 状态过滤,默认为active(可选:active, archived, public_removed) +- `keyword`: 关键词搜索(可选) + +**响应格式**: +```json +{ + "code": 200, + "message": "获取成功", + "data": [ + { + "relation_id": 789, + "creator_id": 456, + "name": "达人姓名", + "avatar": "头像URL", + "category": "Beauty & Personal Care", + "e_commerce_level": "L3", + "exposure_level": "KOL-1", + "followers": "125k", + "gmv": "$534k", + "avg_video_views": "85k", + "pricing": "$200", + "collab_count": 15, + "notes": "私有库备注", + "status": "active", // active, archived, public_removed + "added_from_public": true, + "added_at": "2024-03-15", + "is_public_removed": false, + "status_note": null + } + ], + "pagination": { + "current_page": 1, + "total_pages": 3, + "total_count": 25, + "has_next": true, + "has_prev": false + }, + "pool_info": { + "id": 123, + "name": "我的收藏", + "description": "私有库描述", + "is_default": true, + "user_id": 1, + "created_at": "2024-03-15" + } +} +``` + +### 6.4 添加达人到私有库 +- **接口路径**: `POST /private/pools/creators/add/` +- **功能**: 将达人添加到指定私有库 +- **请求方法**: POST + +**请求体参数**: +```json +{ + "pool_id": 123, // 必填 + "creator_id": 456, // 单个添加时使用 + "creator_ids": [456, 457, 458], // 批量添加时使用 + "notes": "备注信息" // 可选 +} +``` + +**响应格式**: +```json +{ + "code": 200, + "message": "操作成功", + "data": { + "added": [ + { + "id": 456, + "name": "达人姓名", + "action": "添加" + } + ], + "already_exists_count": 2, + "pool": { + "id": 123, + "name": "我的收藏" + } + } +} +``` + +### 6.5 更新私有库中的达人 +- **接口路径**: `POST /private/pools/creators/update/` +- **功能**: 更新私有库中达人的状态或备注 +- **请求方法**: POST + +**请求体参数**: +```json +{ + "relation_id": 789, // 必填 + "status": "archived", // 可选:active, archived + "notes": "新的备注信息" // 可选,传null可清空 +} +``` + +**响应格式**: +```json +{ + "code": 200, + "message": "更新成功", + "data": { + "relation_id": 789, + "creator_id": 456, + "pool_id": 123, + "status": "archived", + "notes": "新的备注信息" + } +} +``` + +### 6.6 从私有库移除达人 +- **接口路径**: `POST /private/pools/creators/remove/` +- **功能**: 从私有库中移除达人 +- **请求方法**: POST + +**请求体参数**: +```json +{ + // 方式1:通过关联ID删除 + "relation_id": 789, // 单个删除 + "relation_ids": [789, 790], // 批量删除 + + // 方式2:通过私有库ID和达人ID删除 + "pool_id": 123, + "creator_id": 456, // 单个删除 + "creator_ids": [456, 457] // 批量删除 +} +``` + +**响应格式**: +```json +{ + "code": 200, + "message": "移除成功", + "data": { + "deleted_count": 2, + "relation_ids": [789, 790], // 或creator_ids + "pool_id": 123 + } +} +``` + +### 6.7 筛选私有库中的达人 +- **接口路径**: `POST /private/pools/creators/filter/` +- **功能**: 根据过滤条件筛选私有库中的达人 +- **请求方法**: POST + +**URL参数**: +- `page`: 页码,默认为1 +- `page_size`: 每页数量,默认为10 + +**请求体参数**: +```json +{ + "pool_id": 123, // 必填 + "filter": { + "status": "active", // 可选:active, archived, public_removed + "category": ["Beauty & Personal Care"], // 多选 + "e_commerce_level": ["L1", "L2", "L3"], // 多选 + "exposure_level": ["KOL-1", "KOC-1"], // 多选 + "gmv_range": ["$0-$5k", "$5k-$25k"], // 多选 + "views_range": ["10k-100k"], // 单选 + "pricing": ["100-500"] // 单选 + } +} +``` + +**响应格式**: 同6.3 + +## 错误响应格式 + +所有接口在出错时都会返回统一的错误格式: + +```json +{ + "code": 400, // 错误状态码 + "message": "错误描述信息", + "data": null +} +``` + +**常见错误码**: +- `400`: 请求参数错误 +- `401`: 用户未认证 +- `403`: 权限不足 +- `404`: 资源不存在 +- `409`: 资源冲突(如重名) +- `500`: 服务器内部错误 + +## 使用示例 + +### 筛选达人示例 +```bash +curl -X POST "https://api.example.com/creators/filter/?page=1" \ + -H "Authorization: Token your_token_here" \ + -H "Content-Type: application/json" \ + -d '{ + "filter": { + "category": ["Beauty & Personal Care"], + "e_commerce_level": ["L2", "L3"], + "gmv_range": ["$5k-$25k", "$25k-$50k"] + } + }' +``` + +### 获取达人详情示例 +```bash +curl -X GET "https://api.example.com/creators/123/" \ + -H "Authorization: Token your_token_here" +``` + +### 更新指标数据示例 +```bash +curl -X POST "https://api.example.com/creators/metrics/update/" \ + -H "Authorization: Token your_token_here" \ + -H "Content-Type: application/json" \ + -d '{ + "creator_id": 123, + "metrics_type": "video", + "metrics_data": { + "start_date": "2024-03-01", + "end_date": "2024-03-31", + "gpm": 2.35, + "videos_count": 12, + "avg_views": 85000, + "avg_engagement": 3.2, + "avg_likes": 2500 + } + }' +``` diff --git a/apps/daren_detail/migrations/0003_alter_privatecreatorrelation_status.py b/apps/daren_detail/migrations/0003_alter_privatecreatorrelation_status.py new file mode 100644 index 0000000..a40380f --- /dev/null +++ b/apps/daren_detail/migrations/0003_alter_privatecreatorrelation_status.py @@ -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='状态'), + ), + ] diff --git a/apps/daren_detail/migrations/0004_creatorprofile_avatar.py b/apps/daren_detail/migrations/0004_creatorprofile_avatar.py new file mode 100644 index 0000000..edb422f --- /dev/null +++ b/apps/daren_detail/migrations/0004_creatorprofile_avatar.py @@ -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='头像图片'), + ), + ] diff --git a/apps/daren_detail/models.py b/apps/daren_detail/models.py index 3bf0856..0e5f920 100644 --- a/apps/daren_detail/models.py +++ b/apps/daren_detail/models.py @@ -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", "公有库已移除") ]) # 时间戳 diff --git a/apps/daren_detail/serializers.py b/apps/daren_detail/serializers.py new file mode 100644 index 0000000..b555a9b --- /dev/null +++ b/apps/daren_detail/serializers.py @@ -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 \ No newline at end of file diff --git a/apps/daren_detail/urls.py b/apps/daren_detail/urls.py index 0269826..a24fae1 100644 --- a/apps/daren_detail/urls.py +++ b/apps/daren_detail/urls.py @@ -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//', views.get_creator_detail, name='get_creator_detail'), - path('creators/update_detail/', views.update_creator_detail, name='update_creator_detail'), - path('creators//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//metrics/', views.get_creator_metrics, name='get_creator_metrics'), - # 获取创作者的所有指标数据 - path('creators/metrics/update/', views.update_creator_metrics, name='update_creator_metrics'), # 更新创作者的指标数据 - - # 添加粉丝统计和趋势数据相关的路由 - path('creator//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//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//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//', 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//', views.get_creator_detail, name='get_creator_detail'), + path('creators/update_detail/', views.update_creator_detail, name='update_creator_detail'), + path('creators//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//metrics/', views.get_creator_metrics, name='get_creator_metrics'), + # 获取创作者的所有指标数据 + path('creators/metrics/update/', views.update_creator_metrics, name='update_creator_metrics'), # 更新创作者的指标数据 + + # 添加粉丝统计和趋势数据相关的路由 + path('creator//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//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//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//', 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'), +] diff --git a/apps/daren_detail/views.py b/apps/daren_detail/views.py index a882ea9..18182de 100644 --- a/apps/daren_detail/views.py +++ b/apps/daren_detail/views.py @@ -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}) + + diff --git a/apps/user/migrations/0005_user_is_superuser.py b/apps/user/migrations/0005_user_is_superuser.py new file mode 100644 index 0000000..ea2be33 --- /dev/null +++ b/apps/user/migrations/0005_user_is_superuser.py @@ -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='超级用户状态'), + ), + ] diff --git a/apps/user/models.py b/apps/user/models.py index 9115417..fe2acec 100644 --- a/apps/user/models.py +++ b/apps/user/models.py @@ -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) diff --git a/daren/settings.py b/daren/settings.py index 8693354..12a241d 100644 --- a/daren/settings.py +++ b/daren/settings.py @@ -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': [], # 默认不需要认证 diff --git a/daren/urls.py b/daren/urls.py index c7f260f..9008c44 100644 --- a/daren/urls.py +++ b/daren/urls.py @@ -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) diff --git a/example_usage.py b/example_usage.py new file mode 100644 index 0000000..50160f9 --- /dev/null +++ b/example_usage.py @@ -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) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index b11861794e368067348437b0b7ec2107475527e5..d2566a6f1e39fed5a8bcf2b589c3fe9e45c19e4c 100644 GIT binary patch delta 78 zcmaFIbBlLF9-DLkLncEG5au(KGuQ&5ArR_;vB6|@CT%rchGL+KT!sRmnpB1?psEtE RI#Zx3V<0wQ;AP-q0054~4bK1o delta 7 Ocmcb``;KQr9vc7-69WbS diff --git a/test_avatar_display.py b/test_avatar_display.py new file mode 100644 index 0000000..a9935b1 --- /dev/null +++ b/test_avatar_display.py @@ -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(""" + +{% for creator in creators %} +
+

{{ creator.name }}

+ {% if creator.get_avatar_url %} + {{ creator.name }}的头像 + {% else %} +
暂无头像
+ {% endif %} +
+{% 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/ 目录存在") \ No newline at end of file