97 KiB
title, source, author, published, created, description, tags, aliases, cssclasses, kanban-plugin, link
| title | source | author | published | created | description | tags | aliases | cssclasses | kanban-plugin | link | ||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| tiktok_pm_project/settings.py | shenwei |
|
#django #python #mysql #mariadb #project #tiktok
title:
style: nestedList # TOC style (nestedList|nestedOrderedList|inlineFirstLevel)
minLevel: 0 # Include headings from the specified level
maxLevel: 0 # Include headings up to the specified level
include:
exclude:
includeLinks: true # Make headings clickable
hideWhenEmpty: false # Hide TOC if no headings are found
debugInConsole: false # Print debug info in Obsidian console
TikTok PM - Python Django 项目
一期规划
- 如果数据从JSON导入,那么 source ID不为空,在admin页面里source id需要变成read-only不可编辑 ✅ 2025-11-21
- description, description1, description2 这些字段需要用富文本框进行编辑和显示 ✅ 2025-11-24
- 在product image和product variation section, 能根据zipline_url直接显示图片缩略图 ✅ 2025-11-21
- 点击图片可以方法看原始尺寸 ✅ 2025-11-21
- 调整zipline_url编辑框尺寸
- 缺少specification字段 ✅ 2025-11-21
- spcifications字段可以根据JSON内容,分别提供编辑框编辑JSON里每一个name,value字段, 默认折叠这个section不显示
- created_at和updated_at都没有值,在没有使用ORM之前该字段没有长度限制,现在是6。如何修改可以让这两个字段自动带上日期时间戳? ✅ 2025-11-21
- 修改过滤条件按,在product list页面可以根据store name进行过滤,去除现有的一些过滤条件比如available, in stock, created at etc. ✅ 2025-11-24
- 增加prodcut_reviews table. ✅ 2025-11-21
- 在product list页面可以批量删除product,包含关联的product_images, product_videos, product_variations, product_reviews ✅ 2025-11-21
- 在product list页面可以高亮final price在10
50之间的产品,整条记录高亮,以区别其他的产品,1050这个区间可以通过过滤条件来修改? - 按不同颜色显示不同sold数量的商品
- 在base info section显示第一张product image
- product list页面,显示每个产品的第一张图片的缩略图, 点击可以打开放大。 ✅ 2025-11-21
- 将整个项目push到Github个人账户下 ✅ 2025-11-22
- 把下载的JSON存放在某个特定目录下 data\json ✅ 2025-11-24
- 根据decription detail生成HTML文件存放在data\html下 ✅ 2025-11-24
- 在产品详情页面增加了view html链接,可以直接打开descrtpion detail html ✅ 2025-11-24
- 继续优化product fetch页面,可以根据不同的抓取方式发送不同的request
- TikTok Shop collect by URL, ✅ 2025-11-24
- TikTok Shop Discover by keywords
- TikTok Shop Discover by profile url
- TikTok Shop Discover by category
- TikTok Shop Discover by shop
- 抓取数据默认设置一次性抓取上限值,放在settings里可以配置
- 用AI设计来分析抓取的数据,并在superset里实现
- 在products表里记录snapshot name这样可以回溯原始的JSON
- 研究Djang可否直接把model导出成JSON(单个产品)以便于被n8n读取
二期规划
- 通过淘宝开放平台拍立淘API来实现,图找商品的功能, 目前看起来浏览器插件可完成
- 在应用里实现按产品source ID来调用Bright Data API获取JSON, 并导入数据库 ✅ 2025-11-24
常用命令
开发环境:
#创建Django Admin登录超级用户
python manage.py createsuperuser
python manage.py makemigrations
python manage.py migrate
python manage.py import_json_data
生产环境
docker compose exec web python manage.py createsuperuser
docker compose exec web python manage.py makemigrations
docker compose exec web python manage.py migrate
docker compose exec web python manage.py import_json_data
0. 项目规划
-
项目初始化:
django-admin startproject tiktok_pm&python manage.py startapp products -
配置数据库: 在
settings.py中配置 MySQL 连接。 -
定义模型: 在
products/models.py中编写 ORM 模型 (参考 2.1)。 -
运行迁移:
python manage.py makemigrations&python manage.py migrate -
注册 Admin: 在
products/admin.py中注册模型并定制搜索/过滤/列表展示。 -
安装 DRF:
pip install djangorestframework -
编写 API: 在
products/serializers.py和products/views.py中编写 DRF 组件。 -
部署: 使用 Gunicorn/uWSGI + Nginx/Apache 进行生产环境部署。
1. 项目准备与软件安装
在开始 Django 项目之前,您需要准备 Python 环境和 MySQL 数据库,并安装必要的 Python 库。
1.1. 核心软件安装
| 软件 | 用途 | 备注 |
|---|---|---|
| Python 3.8+ | 编程语言和运行环境 | 确保安装了较新的版本。 |
| pip | Python 包管理器 | 通常随 Python 一起安装。 |
| MySQL Server | 数据库系统 | 存储所有产品数据 (与您提供的 SQL 文件保持一致)。 |
| Virtual Environment (推荐) | 隔离项目依赖 | 确保每个项目有独立的依赖环境,避免冲突 |
1.2. Python 依赖库安装
我们需要 Django 及其配套的数据库驱动和 RESTful API 框架。
-
打开您的命令行工具 (如 Terminal, CMD, 或 PowerShell)。
-
创建并激活虚拟环境 (强烈推荐):
Bash
# 1. 创建名为 'venv' 的虚拟环境 python -m venv venv # 2. 激活虚拟环境 (Windows) venv\Scripts\activate # 3. 激活虚拟环境 (macOS/Linux) source venv/bin/activate(激活后,您会在命令行前看到
(venv)标识。) -
安装核心依赖库:
Bash
pip install Django djangorestframework mysqlclient-
Django: 核心 Web 框架。 -
djangorestframework: 用于构建 API 接口 (方便 n8n 集成)。 -
mysqlclient: Python 连接 MySQL 数据库的驱动。
-
2. 项目初始化与配置
Django 采用“项目 (Project)”和“应用 (App)”的两级结构。
2.1. 创建 Django 项目 (Project)
项目是整个应用的总配置和环境设置。
-
在命令行中执行以下命令创建项目:
Bash
# 命名为 tiktok_pm_project django-admin startproject tiktok_pm_project .(注意末尾的
.表示在当前目录下创建,结构更清晰。)执行后,您的目录结构如下:
tiktok_pm_project/ ├── manage.py └── tiktok_pm_project/ ├── __init__.py ├── asgi.py ├── settings.py # 核心配置文件 ├── urls.py # 根路由配置 └── wsgi.py
2.2. 创建 Django 应用 (App)
应用是实现具体功能 (如产品管理) 的模块。
-
在命令行中创建应用:
Bash
# 命名为 products,专门处理产品相关逻辑 python manage.py startapp products您的目录结构更新为:
tiktok_pm_project/ ├── manage.py ├── products/ # 新建的应用目录 │ ├── migrations/ │ ├── __init__.py │ ├── admin.py # 配置管理后台 │ ├── apps.py │ ├── models.py # 定义数据库模型 (ORM) │ ├── tests.py │ └── views.py # 编写业务逻辑 (CRUD) └── tiktok_pm_project/ # ... (项目配置文件)
2.3. 基础配置 (settings.py)
打开 tiktok_pm_project/settings.py 文件,进行以下关键配置:
A. 注册应用和框架
找到 INSTALLED_APPS 列表,添加您创建的 products 应用和 djangorestframework。
Python
# tiktok_pm_project/settings.py
INSTALLED_APPS = [
# Django 默认自带
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# 第三方框架
'rest_framework', # DRF (用于API)
# 您的应用
'products', # 注册您的产品应用
]
B. 配置数据库 (MySQL)
找到 DATABASES 配置项,替换为您的 MySQL 连接信息。
Python
# tiktok_pm_project/settings.py
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql', # 数据库引擎
'NAME': 'tiktok_product_db', # 您的数据库名称 (需提前创建)
'USER': 'your_mysql_user', # 您的MySQL用户名
'PASSWORD': 'your_mysql_password', # 您的MySQL密码
'HOST': '127.0.0.1', # 数据库地址
'PORT': '3306', # 数据库端口
'OPTIONS': {
'init_command': "SET sql_mode='STRICT_TRANS_TABLES'",
'charset': 'utf8mb4', # 确保支持emoji和复杂字符
}
}
}
2.4. 首次迁移与创建管理员
-
应用配置的迁移:
Bash
python manage.py migrate(这会创建 Django 自身需要的用户、会话等基础表。)
-
创建超级管理员账户 (用于登录 Admin 后台):
Bash
python manage.py createsuperuser(输入用户名、邮箱和密码。)
#superuser username = admin password = Abcd_1234
2.5. 运行项目
-
启动开发服务器:
Bash
python manage.py runserver -
访问 Admin 后台: 在浏览器中打开
http://127.0.0.1:8000/admin/,使用您刚刚创建的超级管理员账户登录。
3. 定义模型
3.1:保存 products/models.py 文件
from django.db import models
# Create your models here.
from django.db import models
class Product(models.Model):
"""
对应 tables.sql 中的 products 表。
存储 TikTok 平台产品的核心信息。
"""
# 基础信息
# id BIGINT AUTO_INCREMENT PRIMARY KEY 已经由 Django 自动创建
source_id = models.CharField(
max_length=64,
unique=True,
db_index=True, # 确保查询效率
verbose_name="TikTok Source ID"
)
url = models.TextField(blank=True, null=True)
title = models.TextField(blank=True, null=True)
# 描述字段 (使用 TextField 对应 MySQL 的 LONGTEXT/TEXT) description = models.TextField(blank=True, null=True)
description_1 = models.TextField(blank=True, null=True)
description_2 = models.TextField(blank=True, null=True)
desc_detail = models.TextField(blank=True, null=True)
desc_detail_1 = models.TextField(blank=True, null=True)
desc_detail_2 = models.TextField(blank=True, null=True)
# 状态字段
available = models.BooleanField(blank=True, null=True)
In_stock = models.BooleanField(blank=True, null=True) # 字段名与SQL文件保持一致
# 价格字段
currency = models.CharField(max_length=16, blank=True, null=True)
initial_price = models.DecimalField(max_digits=10, decimal_places=2, blank=True, null=True)
final_price = models.DecimalField(
max_digits=10,
decimal_places=2,
blank=True,
null=True,
db_index=True # 对应 idx_price )
discount_percent = models.DecimalField(max_digits=10, decimal_places=2, blank=True, null=True)
# 价格范围字段
initial_price_low = models.DecimalField(max_digits=10, decimal_places=2, blank=True, null=True)
initial_price_high = models.DecimalField(max_digits=10, decimal_places=2, blank=True, null=True)
final_price_low = models.DecimalField(max_digits=10, decimal_places=2, blank=True, null=True)
final_price_high = models.DecimalField(max_digits=10, decimal_places=2, blank=True, null=True)
# 销售和位置
sold = models.IntegerField(blank=True, null=True, db_index=True) # 对应 idx_sold position = models.IntegerField(blank=True, null=True)
# JSON 字段 (存储复杂的结构化数据)
colors = models.JSONField(blank=True, null=True)
sizes = models.JSONField(blank=True, null=True)
shipping_fee = models.JSONField(blank=True, null=True)
specifications = models.JSONField(blank=True, null=True)
videos = models.JSONField(blank=True, null=True)
related_videos = models.JSONField(blank=True, null=True)
# 视频链接
video_link = models.TextField(blank=True, null=True)
# 分类和卖家信息
category = models.CharField(max_length=255, blank=True, null=True, db_index=True) # 对应 idx_category category_url = models.TextField(blank=True, null=True)
seller_id = models.CharField(max_length=128, blank=True, null=True, db_index=True) # 对应 idx_seller store_name = models.CharField(max_length=255, blank=True, null=True)
# 评价和性能
prodct_rating = models.JSONField(blank=True, null=True)
promotion_items = models.JSONField(blank=True, null=True)
shop_performance_metrics = models.JSONField(blank=True, null=True)
# 导入时间戳和原始数据
timestamp = models.DateTimeField(blank=True, null=True)
input = models.JSONField(blank=True, null=True)
raw_json = models.JSONField(blank=True, null=True)
# Django 自动管理的时间戳
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
# 定义模型的复数名称,让Admin后台显示更友好
verbose_name = "TikTok Product"
verbose_name_plural = "TikTok Products"
# 显式指定使用的表名(如果和模型名不一致,可以配置)
db_table = 'products'
def __str__(self):
"""返回对象在Admin后台的显示名称"""
return f"{self.source_id}: {self.title[:50]}..."
# ------------------------------------------------------------
# 提示:其他关联表 (product_images, product_variations 等)
# 需要您参照此格式,继续在 models.py 文件中创建,并设置 ForeignKey 关联。
# ------------------------------------------------------------
# ------------------------------------------------------------
# 1. Table: product_images
# ------------------------------------------------------------
class ProductImage(models.Model):
product = models.ForeignKey(
Product,
on_delete=models.CASCADE, # 对应 ON DELETE CASCADE related_name='images', # 方便从 Product 对象反向查询所有图片:product.images.all()
verbose_name="关联产品"
)
image_type = models.TextField(blank=True, null=True)
original_url = models.TextField(blank=True, null=True)
zipline_url = models.TextField(blank=True, null=True)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = 'product_images'
verbose_name_plural = "产品图片"
def __str__(self):
return f"Image for {self.product.source_id}"
# ------------------------------------------------------------
# 2. Table: product_videos
# ------------------------------------------------------------
class ProductVideo(models.Model):
product = models.ForeignKey(
Product,
on_delete=models.CASCADE, # 对应 ON DELETE CASCADE related_name='videos_list', # 注意:避免与 Product.videos (JSONField) 字段名冲突
verbose_name="关联产品"
)
video_type = models.TextField(blank=True, null=True)
original_url = models.TextField(blank=True, null=True)
zipline_url = models.TextField(blank=True, null=True)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = 'product_videos'
verbose_name_plural = "产品视频"
def __str__(self):
return f"Video for {self.product.source_id}"
# ------------------------------------------------------------
# 3. Table: product_variations
# ------------------------------------------------------------
class ProductVariation(models.Model):
product = models.ForeignKey(
Product,
on_delete=models.CASCADE, # 对应 ON DELETE CASCADE related_name='variations', # 方便从 Product 对象反向查询所有变体:product.variations.all()
verbose_name="关联产品"
)
sku = models.CharField(max_length=128, blank=True, null=True, db_index=True) # 对应 idx_variation_sku sku_sales_props = models.JSONField(blank=True, null=True)
stock = models.IntegerField(blank=True, null=True)
purchase_limit = models.IntegerField(blank=True, null=True)
initial_price = models.DecimalField(max_digits=10, decimal_places=2, blank=True, null=True)
final_price = models.DecimalField(max_digits=10, decimal_places=2, blank=True, null=True)
currency = models.CharField(max_length=16, blank=True, null=True)
discount_percent = models.DecimalField(max_digits=10, decimal_places=2, blank=True, null=True)
image_original_url = models.TextField(blank=True, null=True)
image_zipline_url = models.TextField(blank=True, null=True)
created_at = models.DateTimeField(auto_now_add=True)
# auto_now=True 对应 ON UPDATE CURRENT_TIMESTAMP updated_at = models.DateTimeField(auto_now=True)
class Meta:
db_table = 'product_variations'
verbose_name_plural = "产品变体"
def __str__(self):
return f"Variation SKU: {self.sku} ({self.product.source_id})"
在 Django 中,要将您对 models.py 文件中 ORM 模型的修改同步到数据库,您必须使用 数据库迁移 (Migrations) 系统。
Django 会自动检测您对模型(例如添加、修改或删除字段)所做的更改,并为您生成必要的 SQL 语句。
对于您修改 created_at = models.DateTimeField(auto_now_add=True) 为 created_at = models.DateTimeField(auto_now_add=True, null=True) 的操作,需要执行以下两个步骤:
创建迁移文件 (Make Migrations)
这个命令会扫描您的 products/models.py 文件,检测到字段的变化,并在 products/migrations/ 目录下创建一个新的 Python 文件,记录这个变更(即添加 null=True 属性)。
Bash
# 在项目的根目录下(manage.py 所在目录)执行
python manage.py makemigrations products
执行结果示例:
Migrations for 'products':
products/migrations/000x_..._creatd_at_nullable.py # 文件名可能不同
- Alter field created_at on product
执行迁移到数据库 (Migrate)
这个命令会读取上一步生成的迁移文件,并根据其中的指令,执行相应的 SQL 命令来修改您 MySQL 数据库中现有表的结构。
Bash
# 执行所有待处理的迁移
python manage.py migrate
执行结果示例:
Operations to perform:
Apply all migrations: admin, auth, contenttypes, sessions, products
Running migrations:
Applying products.000x_..._creatd_at_nullable... OK
3.2 总结
无论是修改字段类型、添加新字段,还是像您这样修改字段约束(例如从非空改为可空 null=True),都必须遵循这两步流程:
-
makemigrations <app_name>:记录模型更改。 -
migrate:应用更改到数据库。
注意: 您修改 created_at 字段并添加 null=True 后,如果该字段之前已经有数据,数据库本身可能不会立即受到影响(因为 auto_now_add=True 意味着它在创建时总会被赋值)。但将 null=True 添加到模型中是正确的做法,尤其是在您未来可能手动操作这个字段时,它告诉 Django 数据库该字段是可空的。
如果您的更改是添加了一个非空的新字段,Django 会在 makemigrations 步骤中询问您如何处理现有行的数据。
-
您可以提供一个默认值。
-
或者在迁移文件中手动设置
default=参数。
4 运行迁移
您需要再次执行以下两个命令,将新定义的 ORM 模型转换为数据库表:
4.1: 创建新的迁移文件 (Migration):
python manage.py makemigrations products
(Django 会检测到 ProductImage, ProductVideo, 和 ProductVariation 三个新模型。)_
4.2: 执行迁移到数据库:
python manage.py migrate
(这会在您的 MySQL 数据库中创建 product_images, product_videos, 和 product_variations 三个新表,并设置好外键关系。)_
5 注册 Admin
Django Admin 是一个基于模型自动生成的管理界面,非常适合作为管理员工具(Admin Management Tool)。
您需要编辑 products/admin.py 文件。
5.1. 配置 Admin 后台
打开您的 products/admin.py 文件,并按以下步骤添加代码。
from django.contrib import admin
# Register your models here.
from django.contrib import admin
# --- 导入富文本字段 ---from tinymce.widgets import TinyMCE
# --- 导入 forms 模块 ---from django import forms
# --- 新增导入:用于渲染HTML ---
from django.utils.safestring import mark_safe
from .models import (
Product,
ProductImage,
ProductVideo,
ProductVariation
)
# ----------------------------------------------------------------------
# 核心类: 定义富文本表单 (用于 ProductAdmin)# ----------------------------------------------------------------------
class ProductAdminForm(forms.ModelForm):
"""
定义 Product 模型的自定义表单,指定字段使用 TinyMCE 插件。
""" class Meta:
model = Product
fields = '__all__'
# 将描述字段指定为 TinyMCE 富文本 widget widgets = {
'description': TinyMCE(attrs={'cols': 80, 'rows': 20}),
'description_1': TinyMCE(attrs={'cols': 80, 'rows': 15}),
'description_2': TinyMCE(attrs={'cols': 80, 'rows': 10}),
}
# ----------------------------------------------------------------------
# 辅助类: 用于在 Product 详情页内嵌显示关联信息
# ----------------------------------------------------------------------
# 1. 产品图片 (ProductImages)class ProductImageInline(admin.TabularInline):
"""在 Product 编辑页内嵌展示产品图片"""
model = ProductImage
# 指定显示的字段
fields = ('image_type', 'original_url', 'zipline_url', 'image_preview', 'created_at')
readonly_fields = ('image_preview', 'created_at',)
extra = 1 # 额外显示一行空白表单供新增
def image_preview(self, obj):
if obj.zipline_url:
return mark_safe(f'<img src="{obj.zipline_url}" style="max-width: 100px; max-height: 100px;" />')
return "No Image"
image_preview.short_description = 'Preview'
# 2. 产品视频 (ProductVideos)class ProductVideoInline(admin.TabularInline):
"""在 Product 编辑页内嵌展示产品视频"""
model = ProductVideo
fields = ('video_type', 'original_url', 'zipline_url', 'created_at')
readonly_fields = ('created_at',)
extra = 1
# 3. 产品变体 (ProductVariations)class ProductVariationInline(admin.TabularInline):
"""在 Product 编辑页内嵌展示产品变体/SKU"""
model = ProductVariation
# fields 列表添加 'image_preview' fields = (
'sku', 'stock', 'purchase_limit', 'final_price', 'currency',
'image_original_url', 'image_preview', 'updated_at' # <-- 添加 image_preview )
# readonly_fields 列表添加 'image_preview' readonly_fields = ('image_preview', 'updated_at',)
extra = 1
def image_preview(self, obj):
"""根据 image_zipline_url 生成图片预览"""
if obj.image_zipline_url:
return mark_safe(f'<img src="{obj.image_zipline_url}" style="max-width: 100px; max-height: 100px;" />')
return "No Image"
image_preview.short_description = 'Preview'
# 可以通过设置 max_num 来限制变体的数量,这里不设置,保持可拓展性
# ----------------------------------------------------------------------
# 核心类: 定制 Product 模型的管理界面 (CRUD)# ----------------------------------------------------------------------
@admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
# --- 新增: 引用自定义表单 --- form = ProductAdminForm
# =========================================================
# 列表页定制 (list view) # =========================================================
# 1. 列表显示的字段 (要求 3.2: 查询)
list_display = (
'source_id',
'title_short',
'store_name',
'final_price',
'sold',
'available',
'In_stock',
'created_at'
)
# 2. 链接到编辑页的字段
list_display_links = ('source_id', 'title_short')
# 3. 快速关键词搜索 (要求 3.8: 快速搜索)
# 配置搜索栏将根据哪些字段进行模糊查询 (LIKE) search_fields = (
'source_id',
'title',
'store_name',
'category',
'seller_id'
)
# 4. 多条件过滤搜索 (要求 3.9: 多条件过滤)
# 配置侧边栏过滤器,允许用户根据这些字段筛选数据
list_filter = (
'available',
'In_stock',
'category',
'currency',
'created_at',
'final_price' # 价格范围过滤可能需要安装第三方库,这里先用默认过滤器
)
# 5. 列表页可编辑字段
list_editable = ('available', 'In_stock',)
# 6. 每页显示数量
list_per_page = 25
# 7. 优化显示 title 字段
def title_short(self, obj):
"""截取 title 字段,使其在列表页显示更简洁"""
return obj.title[:50] + '...' if obj.title and len(obj.title) > 50 else obj.title
title_short.short_description = 'Title' # 定义列的标题
# =========================================================
# 详情页定制 (Change/Add view) # =========================================================
# 1. 字段分组显示
fieldsets = (
('Product Base Info', {
'fields':(
('source_id', 'title', 'url', 'category', 'category_url', 'position'),
('colors', 'sizes', 'shipping_fee', 'specifications'),
)
}),
('Sell Status', {
'fields': (
('available', 'In_stock'),
('sold',)
),
}),
('Price Settings', {
# 'classes': ('collapse',), # 默认折叠该部分
'fields': (
('currency', 'initial_price', 'final_price', 'discount_percent'),
('initial_price_low', 'initial_price_high'),
('final_price_low', 'final_price_high'),
),
}),
('Seller Info', {
'fields': ('seller_id', 'store_name', 'shop_performance_metrics'),
}),
('Descriptions', {
'fields': ('description', 'description_1', 'description_2'),
# 备注: 此处需要集成富文本编辑器 (如 TinyMCE) 来保留格式
}),
('JSON Raw Data', {
'classes': ('collapse',), # 默认折叠,减少页面冗余
'fields': ('input', 'raw_json'),
}),
)
# 2. 内联关联模型 (显示关联的图片、视频、变体)
inlines = [
ProductVariationInline,
ProductImageInline,
ProductVideoInline
]
# 3. 不允许修改的字段
readonly_fields = ('source_id', 'created_at', 'updated_at')
# ----------------------------------------------------------------------
# 注册其他独立关联模型 (如果需要独立管理和搜索)
# ----------------------------------------------------------------------
# 尽管 ProductVariation 已内嵌,但最好也注册独立管理
@admin.register(ProductVariation)
class ProductVariationAdmin(admin.ModelAdmin):
list_display = ('sku', 'product', 'stock', 'final_price', 'updated_at')
list_filter = ('stock', 'final_price')
search_fields = ('sku', 'product__source_id') # 支持跨模型搜索
raw_id_fields = ('product',) # 使用ID输入框选择产品,提升性能
6. 富文本编辑器集成
要实现 description 字段的富文本编辑和显示,我们通常使用第三方库。这里推荐使用 django-tinymce,它易于安装和集成。
6.1: 安装 django-tinymce
在您的虚拟环境中,使用 pip 安装:
Bash
pip install django-tinymce
6.2: 注册应用
打开您的项目配置文件 tiktok_pm_project/settings.py,并在 INSTALLED_APPS 中添加 'tinymce':
Python
# tiktok_pm_project/settings.py
INSTALLED_APPS = [
# ... 其他应用
'rest_framework',
# 富文本编辑器
'tinymce', # <-- 新增
'products',
]
6.3: 修改 ProductAdmin
我们需要从 tinymce 导入专用的表单字段,并覆盖 ProductAdmin 的表单属性。
打开 products/admin.py,进行以下修改:
Python
# products/admin.py
from django.contrib import admin
# --- 导入富文本字段 ---
from tinymce.widgets import TinyMCE
# --- 导入 forms 模块 ---
from django import forms
from .models import (
Product,
ProductImage,
ProductVideo,
ProductVariation
)
# ... (ProductImageInline, ProductVideoInline, ProductVariationInline 保持不变)
# ----------------------------------------------------------------------
# 核心类: 定义富文本表单 (用于 ProductAdmin)
# ----------------------------------------------------------------------
class ProductAdminForm(forms.ModelForm):
"""
定义 Product 模型的自定义表单,指定字段使用 TinyMCE 插件。
"""
class Meta:
model = Product
fields = '__all__'
# 将描述字段指定为 TinyMCE 富文本 widget
widgets = {
'description': TinyMCE(attrs={'cols': 80, 'rows': 20}),
'description_1': TinyMCE(attrs={'cols': 80, 'rows': 15}),
'description_2': TinyMCE(attrs={'cols': 80, 'rows': 10}),
}
@admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
# --- 新增: 引用自定义表单 ---
form = ProductAdminForm
# =========================================================
# ... (list_display, search_fields, list_filter 等保持不变)
# =========================================================
# ... (fieldsets, inlines, readonly_fields 等保持不变)
# ... (ProductVariationAdmin 保持不变)
7. 实现 API 接口
现在您的模型和管理后台已经非常完善了。接下来,我们来实现 Django REST Framework (DRF) 接口,以便满足 n8n 等第三方应用调用和融入自动化的要求.
7.1: 配置 URL
打开您的项目根目录下的 tiktok_pm_project/urls.py 文件,添加 DRF 的配置:
Python
# tiktok_pm_project/urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
# 将所有 API 路由包含进来
path('api/', include('products.urls')), # <-- 新增 API 入口
# 如果需要,可以添加 DRF 默认的登录/认证页面
path('api-auth/', include('rest_framework.urls', namespace='rest_framework')),
]
7.2: 创建应用 URL 文件
在您的 products 应用目录下创建一个新文件 products/urls.py,用于定义 API 路由:
Python
# products/urls.py
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from . import views
# 使用 DefaultRouter 自动生成标准的 CRUD 路由 (GET/POST/PUT/DELETE)
router = DefaultRouter()
# 注册 ProductViewSet,生成 /products/ 和 /products/{id}/ 路由
router.register(r'products', views.ProductViewSet)
# 注册 ProductVariationViewSet,生成 /variations/ 和 /variations/{id}/ 路由
router.register(r'variations', views.ProductVariationViewSet)
# DRF 最佳实践:使用 ViewSet 和 Router 自动构建 API
urlpatterns = [
path('', include(router.urls)),
]
7.3: 定义序列化器 (Serializers)
序列化器用于将 Django 模型实例(Python 对象)转换为 JSON/XML 格式,以便通过 API 传输。
创建新文件 products/serializers.py:
Python
# products/serializers.py
from rest_framework import serializers
from .models import Product, ProductVariation, ProductImage, ProductVideo
# --- 辅助序列化器 (用于嵌套展示) ---
class ProductImageSerializer(serializers.ModelSerializer):
class Meta:
model = ProductImage
fields = ['id', 'image_type', 'original_url', 'zipline_url']
read_only_fields = ['id']
class ProductVideoSerializer(serializers.ModelSerializer):
class Meta:
model = ProductVideo
fields = ['id', 'video_type', 'original_url', 'zipline_url']
read_only_fields = ['id']
class ProductVariationSerializer(serializers.ModelSerializer):
class Meta:
model = ProductVariation
# 排除 product_id 字段,因为它会被 ProductSerializer 自动处理
exclude = ['product', 'created_at']
# --- 核心产品序列化器 (用于 Product API) ---
class ProductSerializer(serializers.ModelSerializer):
# 将关联模型嵌套进来,方便 n8n 一次性获取所有信息
images = ProductImageSerializer(many=True, read_only=True) # related_name='images'
variations = ProductVariationSerializer(many=True, read_only=True) # related_name='variations'
videos_list = ProductVideoSerializer(many=True, read_only=True) # related_name='videos_list'
class Meta:
model = Product
# 排除 'input', 'raw_json' 等大型或内部字段,简化常用 API 响应
# 如果需要,可以创建 ProductDetailSerializer 来包含它们
exclude = ['raw_json', 'input', 'timestamp']
# 确保 created_at, updated_at, source_id, seller_id 等关键字段可读
read_only_fields = ['id', 'created_at', 'updated_at']
7.4: 定义视图集 (ViewSets)
视图集定义了 API 的业务逻辑(CRUD)。
打开 products/views.py:
Python
# products/views.py
from rest_framework import viewsets
from rest_framework.permissions import IsAuthenticated
from .models import Product, ProductVariation
from .serializers import ProductSerializer, ProductVariationSerializer
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import filters
class ProductViewSet(viewsets.ModelViewSet):
"""
提供 Product 资源的 CRUD 操作 API。
实现:快速搜索 (要求 3.8),多条件过滤 (要求 3.9)
"""
queryset = Product.objects.all().order_by('-updated_at')
serializer_class = ProductSerializer
# 限制只有认证用户才能访问 API
# permission_classes = [IsAuthenticated]
# 启用过滤和搜索后端
filter_backends = [DjangoFilterBackend, filters.SearchFilter]
# 启用字段过滤(多条件过滤)
filterset_fields = [
'available',
'In_stock',
'category',
'seller_id',
'final_price',
]
# 启用快速搜索 (要求 3.8)
search_fields = [
'=source_id', # 精确匹配
'title',
'store_name',
'description'
]
# 可选:自定义查询集以提高性能
def get_queryset(self):
# 预加载关联数据以解决 N+1 查询问题
return Product.objects.all().select_related().prefetch_related(
'images', 'variations', 'videos_list'
).order_by('-updated_at')
class ProductVariationViewSet(viewsets.ModelViewSet):
"""
提供 ProductVariation 资源的 CRUD 操作 API。
"""
queryset = ProductVariation.objects.all()
serializer_class = ProductVariationSerializer
# 启用字段过滤
filter_backends = [DjangoFilterBackend, filters.SearchFilter]
filterset_fields = ['product', 'sku', 'stock']
search_fields = ['=sku', 'product__source_id']
7.5: 安装依赖
为实现高级过滤(要求 3.9),您需要安装 django-filter 库:
Bash
pip install django-filter
并在 tiktok_pm_project/settings.py 中注册:
Python
# tiktok_pm_project/settings.py
INSTALLED_APPS = [
# ...
'rest_framework',
'tinymce',
'products',
# API 过滤
'django_filters', # <-- 新增
]
# 配置 DRF 使用 DjangoFilter
REST_FRAMEWORK = {
'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend']
}
现在,您的 Web Application 已经具备了完整的 Admin 后台管理界面和可用于自动化的 RESTful API 接口,完全满足了您的所有要求!
API 示例路由:
-
查询所有产品:
GET /api/products/ -
关键词搜索 (n8n 调用):
GET /api/products/?search=关键词 -
多条件过滤:
GET /api/products/?category=服饰&In_stock=true -
创建产品:
POST /api/products/(发送 JSON 数据)
8. 模态框图片放大实现方案
我们需要创建两个文件:
-
admin_image_modal.css: 定义模态框的样式。 -
admin_image_modal.js: 编写 JavaScript 逻辑,捕获点击事件并显示模态框。
步骤 A: 创建静态文件目录结构
首先,在您的 products 应用目录下创建静态文件所需的路径:
products/
└── static/
└── admin/
└── css/
└── admin_image_modal.css # <-- CSS 文件
└── js/
└── admin_image_modal.js # <-- JS 文件
步骤 B: 编写 CSS (admin_image_modal.css)
将以下代码粘贴到 products/static/admin/css/admin_image_modal.css 中。
CSS
/* 模态框背景,覆盖整个屏幕 */
.image-modal {
display: none; /* 默认隐藏 */
position: fixed;
z-index: 9999; /* 确保在最上层 */
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgba(0, 0, 0, 0.9); /* 黑色半透明背景 */
}
/* 模态框内容:图片居中 */
.image-modal-content {
margin: auto;
display: block;
width: 80%;
max-width: 900px;
max-height: 90vh; /* 限制最大高度为视口高度的 90% */
object-fit: contain; /* 确保图片在框内完整显示 */
position: relative;
top: 50%;
transform: translateY(-50%);
}
/* 关闭按钮 */
.image-modal-close {
position: absolute;
top: 15px;
right: 35px;
color: #f1f1f1;
font-size: 40px;
font-weight: bold;
transition: 0.3s;
cursor: pointer;
z-index: 10000;
}
.image-modal-close:hover,
.image-modal-close:focus {
color: #bbb;
text-decoration: none;
}
步骤 C: 编写 JavaScript (admin_image_modal.js)
将以下代码粘贴到 products/static/admin/js/admin_image_modal.js 中。
JavaScript
/* products/static/admin/js/admin_image_modal.js - 最终版本 */
// 我们不再依赖 IIFE 的参数映射,而是直接使用 django.jQuery// 使用 setTimeout 确保我们的代码在 Admin 的其余 JS 之后运行
setTimeout(function() {
// 再次检查,确保 django.jQuery 确实存在
if (typeof django === 'undefined' || typeof django.jQuery === 'undefined') {
console.error("致命错误:无法找到 django.jQuery。图片放大功能失效。");
return;
}
const $ = django.jQuery; // 在这里定义一个局部变量 $ 方便后续代码书写
$(document).ready(function() {
// 1. 在页面底部注入模态框 HTML 结构
$('body').append(`
<div id="productImageModal" class="image-modal"> <span class="image-modal-close">×</span> <img class="image-modal-content" id="imgModalContent"> </div> `);
var modal = $('#productImageModal');
var modalImg = $('#imgModalContent');
var closeModal = $('.image-modal-close');
// 2. 捕获图片点击事件
// 使用 #content-main 作为父元素监听事件,阻止事件冒泡干扰 Admin 内部逻辑
$('#content-main').on('click', '.image-clickable', function(e) {
e.preventDefault();
e.stopPropagation();
var clickedImg = $(this);
var largeSrc = clickedImg.data('large-url');
if (largeSrc) {
modal.css('display', 'block');
modalImg.attr('src', largeSrc);
modal.focus();
}
});
// 3. 监听关闭事件
closeModal.on('click', function() {
modal.css('display', 'none');
});
// 点击模态框背景关闭
modal.on('click', function(e) {
if ($(e.target).hasClass('image-modal')) {
modal.css('display', 'none');
}
});
// 键盘 Esc 键关闭
$(document).on('keydown', function(e) {
if (e.key === "Escape" && modal.css('display') === 'block') {
modal.css('display', 'none');
}
});
});
}, 0); // 使用 setTimeout 确保代码在浏览器 Event Loop 的下一个 tick 执行
步骤 D: 修改 Admin Python 代码 (products/admin.py)
我们需要进行两处修改:
-
在
ProductAdmin类中添加Media内部类,引入新的 CSS 和 JS 文件。 -
修改
image_preview方法,使其不再生成<a>标签,而是生成带有data-large-url属性和image-clickableclass 的<img>标签。
Python
# products/admin.py
from django.contrib import admin
from tinymce.widgets import TinyMCE
from django import forms
from django.utils.safestring import mark_safe
# ... 导入保持不变
# ----------------------------------------------------------------------
# 1. 修改 ProductImageInline
# ----------------------------------------------------------------------
class ProductImageInline(admin.TabularInline):
# ... 保持不变 ...
def image_preview(self, obj):
"""生成图片预览,并添加 JavaScript 点击事件支持"""
if obj.zipline_url:
# 不再使用 <a> 标签,而是使用带 data 属性的 <img> 标签
return mark_safe(f'''
<img
src="{obj.zipline_url}"
data-large-url="{obj.zipline_url}"
class="image-clickable"
style="max-width: 100px; max-height: 100px; cursor: pointer;"
title="点击查看大图"
/>
''')
return "No Image"
image_preview.short_description = '预览'
# ----------------------------------------------------------------------
# 2. 修改 ProductVariationInline
# ----------------------------------------------------------------------
class ProductVariationInline(admin.TabularInline):
# ... 保持不变 ...
def image_preview(self, obj):
"""根据 image_zipline_url 生成图片预览,并添加 JavaScript 点击事件支持"""
if obj.image_zipline_url:
# 不再使用 <a> 标签
return mark_safe(f'''
<img
src="{obj.image_zipline_url}"
data-large-url="{obj.image_zipline_url}"
class="image-clickable"
style="max-width: 100px; max-height: 100px; cursor: pointer;"
title="点击查看大图"
/>
''')
return "No Image"
image_preview.short_description = '预览'
# ----------------------------------------------------------------------
# 3. 核心类: 定制 Product 模型的管理界面,注入 CSS/JS
# ----------------------------------------------------------------------
# ... ProductAdminForm 保持不变 ...
@admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
form = ProductAdminForm
# ... 其他配置保持不变 ...
class Media:
"""
引入自定义的静态文件。
注意:Django Admin 默认会加载 jQuery。
"""
css = {
'all': ('admin/css/admin_image_modal.css',)
}
js = (
'admin/js/admin_image_modal.js',
)
# ... ProductVariationAdmin 保持不变 ...
步骤 E: 运行验证
-
收集静态文件: 由于您添加了新的静态文件,如果您的项目部署在生产环境中,需要运行:
Bash
python manage.py collectstatic(在开发环境中通常不需要,但这是一个好习惯。)
-
启动服务器:
Bash
python manage.py runserver -
测试: 登录 Admin 后台,进入产品编辑页面,点击产品图片预览。图片现在应该会在一个覆盖整个屏幕的模态框中放大显示。您可以点击右上角的
×或按Esc键来关闭模态框。
这样,您就实现了简洁且用户体验更好的图片放大功能。
9. 新增 product_reviews 表是一个关键的步骤,用于存储产品的评价数据。
我们将按照标准的 Django 流程来完成这个任务:创建模型、配置关联、数据库迁移,并将其集成到 Admin 后台。
9.1 整体步骤概览
-
products/models.py: 定义新的ProductReview模型。 -
数据库迁移: 运行
makemigrations和migrate创建新表。 -
products/admin.py: 注册ProductReview模型,并将其内联到Product编辑页。
9.2: 创建 ProductReview 模型
打开您的 products/models.py 文件,添加以下 ProductReview 模型定义。
📄 products/models.py
Python
from django.db import models
from django.db.models.functions import Now # 用于 db_default
# from django.utils import timezone # 暂时不需要 timezone.now
# ... (Product, ProductImage, ProductVideo, ProductVariation 模型定义)
# ----------------------------------------------------------------------
# 新增模型: ProductReview (用于存储产品评价)
# ----------------------------------------------------------------------
class ProductReview(models.Model):
# 关联到 Product 表
product = models.ForeignKey(
'Product',
on_delete=models.CASCADE,
related_name='reviews' # 定义反向关联名称
)
# 评价人信息
reviewer_name = models.CharField(
max_length=255,
blank=True,
null=True,
verbose_name='评价人昵称'
)
# 评分 (TINYINT 对应 SmallIntegerField 或 IntegerField)
rating = models.SmallIntegerField(
blank=True,
null=True,
verbose_name='评分 (1-5)'
)
# 评价内容 (TEXT 对应 TextField)
review_text = models.TextField(
blank=True,
null=True,
verbose_name='评价内容'
)
# 评价日期
review_date = models.DateTimeField(
blank=True,
null=True,
verbose_name='评价发生日期'
)
# 原始图片 URL (JSON 对应 JSONField)
images = models.JSONField(
blank=True,
null=True,
verbose_name='原始图片JSON'
)
# Zipline 加速后图片 URL (JSON 对应 JSONField)
zipline_images = models.JSONField(
blank=True,
null=True,
verbose_name='加速图片JSON'
)
# 创建时间 (对应 TIMESTAMP DEFAULT CURRENT_TIMESTAMP)
# 采用之前针对 MySQL DDL 的解决方案:依赖数据库的 DEFAULT
created_at = models.DateTimeField(
blank=True,
null=True,
db_default=Now() # 如果是 Django 4.2+,使用 Now()
# 如果不是 4.2+,请用 models.DateTimeField(blank=True, null=True) 代替
)
class Meta:
verbose_name = '产品评价'
verbose_name_plural = '产品评价'
db_table = 'product_reviews' # 确保表名与您 DDL 中的名称一致
ordering = ['-created_at']
def __str__(self):
return f'{self.product.source_id} - {self.reviewer_name} ({self.rating}星)'
9.3: 执行数据库迁移
保存 products/models.py 后,执行以下两个命令来创建 product_reviews 表。
1. 创建迁移文件
Bash
python manage.py makemigrations products
2. 应用迁移到数据库
Bash
python manage.py migrate
(执行成功后,product_reviews 表将在数据库中创建。)
9.4: 配置 Admin 后台
我们将采用内联方式,让您可以在编辑 Product 时,直接查看和管理其关联的所有 ProductReview。
打开您的 products/admin.py 文件,进行以下修改:
📄 products/admin.py
1. 新增 ProductReviewInline 辅助类
在 ProductImageInline 等内联类定义的下方,添加 ProductReviewInline:
Python
# products/admin.py
# ... (在 ProductVideoInline 和 ProductVariationInline 之后)
# ----------------------------------------------------------------------
# 4. 产品评价 (ProductReviews)
# ----------------------------------------------------------------------
class ProductReviewInline(admin.TabularInline):
"""在 Product 编辑页内嵌展示产品评价"""
model = ProductReview
# 注意:images 和 zipline_images 是 JSONField,如果内容过大,可以不显示或使用定制 widget
fields = (
'reviewer_name', 'rating', 'review_date', 'review_text',
'images', 'zipline_images', 'created_at'
)
readonly_fields = ('created_at',)
extra = 0 # 评价通常是导入的,默认不提供额外空白行
# 如果 review_text 过长,可以使用以下方式控制其显示:
# formfield_overrides = {
# models.TextField: {'widget': forms.Textarea(attrs={'rows': 2, 'cols': 50})}
# }
2. 将 ProductReviewInline 添加到 ProductAdmin
在 @admin.register(Product) 下的 ProductAdmin 类中,将新的内联类添加到 inlines 列表中:
Python
# products/admin.py
@admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
# ... (其他配置保持不变)
# 2. 内联关联模型 (显示关联的图片、视频、变体、评价)
inlines = [
ProductVariationInline,
ProductImageInline,
ProductVideoInline,
ProductReviewInline, # <-- 新增这一行
]
# ... (其他配置保持不变)
9.5 运行验证
-
启动开发服务器:
python manage.py runserver。 -
访问 Admin 后台,进入任何一个产品的编辑页。
-
您应该能在页面底部看到一个新的可折叠的 "产品评价 (Product reviews)" 内联表单,您可以手动添加数据或查看通过导入工具创建的数据。
至此,您已成功将 product_reviews 表集成到您的 Django 项目中。
10 修改过滤条件
好的,这是一个清晰的管理界面定制要求。您希望在 Product 列表页的侧边栏过滤器中,用 store_name (店铺名称) 替换掉现有的 available, In_stock, 和 created_at 过滤条件。
这个修改只需要在 products/admin.py 文件中进行。
10.1 解决方案:修改 ProductAdmin 的 list_filter
打开您的 products/admin.py 文件,找到 ProductAdmin 类的定义,并修改 list_filter 属性如下:
Python
# products/admin.py
@admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
# =========================================================
# 列表页定制 (list view)
# =========================================================
# 1. 列表显示的字段 (保持不变,如果需要)
list_display = (
'source_id',
'title_short',
'store_name', # 确认 'store_name' 在 list_display 中,方便查看
'final_price',
'sold',
'available',
'In_stock',
'created_at'
)
# ... (其他设置如 search_fields 保持不变)
# 4. 多条件过滤搜索 (要求 3.9: 多条件过滤)
# 移除: 'available', 'In_stock', 'created_at'
# 新增: 'store_name'
list_filter = (
'store_name', # <-- 新增:根据店铺名称过滤
'category',
'currency',
'final_price', # 价格过滤
)
# ... (其他设置保持不变)
10.2 为什么 store_name 适用作过滤条件?
store_name 是一个 CharField(或 VARCHAR),当它作为 list_filter 的过滤条件时,Django Admin 会自动创建一个侧边栏,列出数据库中出现过的所有不同的店铺名称,并允许用户点击进行筛选。这对于管理多个店铺的商品非常实用。
保存文件并重启您的开发服务器,即可在 Admin 后台的 Product 列表页右侧看到更新后的过滤侧边栏。
11. Product List页面每个产品添加缩略图
这是一个非常实用的需求,能极大提升产品列表页的可视化效果。我们将利用之前创建的 ProductImage 模型和图片放大功能来实现。
要实现在 Product 列表页 显示第一张图片的缩略图并支持点击放大,我们需要做两处核心修改:
-
在
products/admin.py中:-
在
ProductAdmin类中添加一个方法,用于获取并渲染产品的第一张图片。 -
将该方法添加到
list_display列表中。
-
-
在
products/models.py中:- 优化
Product模型,添加一个属性快速获取第一张图片 URL。
- 优化
11.1: 优化 products/models.py (获取第一张图片)
为了效率,我们确保 Product 模型有一个简单的方法来获取其关联的缩略图 URL。
打开您的 products/models.py 文件,在 Product 模型的定义中添加一个属性:
Python
# products/models.py
# ... (其他导入保持不变)
class Product(models.Model):
# ... (其他字段保持不变)
# ... (Meta 和 __str__ 方法保持不变)
# ------------------------------------------------------------
# 新增属性:获取第一张图片的 Zipline URL (用于列表页展示)
# ------------------------------------------------------------
@property
def first_image_zipline_url(self):
"""返回关联的第一张图片的加速 URL,如果存在的话。"""
# 由于我们在 ProductImage 模型中使用了 related_name='product_images' (假设如此)
# 我们按 id 或某个顺序字段获取第一张图片
first_image = self.product_images.filter(image_type='main').order_by('id').first()
if first_image and first_image.zipline_url:
return first_image.zipline_url
# 如果没有找到 'main' 类型的图片,尝试找任意一张
first_image = self.product_images.order_by('id').first()
if first_image and first_image.zipline_url:
return first_image.zipline_url
return None
# ... (其他模型保持不变)
# 确保 ProductImage 模型有一个正确的 related_name (检查 ProductImage 模型)
# class ProductImage(models.Model):
# product = models.ForeignKey(..., related_name='product_images', ...)
(⚠️ 重要提醒:) 请检查您的 ProductImage 模型中,指向 Product 的外键是否使用了 related_name='product_images'。如果没有,请将其添加到 ProductImage 模型中:
Python
# products/models.py (在 ProductImage 模型中)
class ProductImage(models.Model):
# ...
product = models.ForeignKey(
'Product',
on_delete=models.CASCADE,
related_name='product_images' # <--- 确保有这个 related_name
)
# ...
11.2: 修改 products/admin.py (渲染列表页缩略图)
打开您的 products/admin.py 文件,进行以下修改:
📄 products/admin.py
1. 新增方法 product_thumbnail
在 ProductAdmin 类中添加一个新方法,该方法将使用我们之前为内联表单设置的 点击放大 的 HTML 结构。
Python
# products/admin.py
from django.utils.safestring import mark_safe
# ... (确保顶部导入了 mark_safe)
@admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
# ... (其他设置保持不变)
# ------------------------------------------------------------
# 列表页自定义方法:产品缩略图(支持点击放大)
# ------------------------------------------------------------
def product_thumbnail(self, obj):
"""显示产品的第一张图片缩略图,并应用点击放大 JS 样式"""
img_url = obj.first_image_zipline_url # 调用 models.py 中定义的属性
if img_url:
# 使用我们在 Admin 中定义的图片点击放大样式 (class="image-clickable")
return mark_safe(f'''
<img
src="{img_url}"
data-large-url="{img_url}"
class="image-clickable"
style="max-width: 60px; max-height: 60px; cursor: pointer;"
title="点击查看大图"
/>
''')
return "N/A"
product_thumbnail.short_description = '图片' # 列表页列名
product_thumbnail.allow_tags = True
# ------------------------------------------------------------
# 列表页显示字段 (list_display)
# ------------------------------------------------------------
list_display = (
'product_thumbnail', # <-- 新增:将缩略图放在第一列
'source_id',
'title_short',
'store_name',
'final_price',
'sold',
'available',
'In_stock',
'created_at',
'price_highlight_class'
)
# ... (其他设置保持不变)
2. 将 product_thumbnail 添加到 list_display
确保在 list_display 列表中,您将 'product_thumbnail' 添加到了您希望显示的位置(通常是第一列)。
运行验证
-
重启服务器:
Bash
python manage.py runserver -
访问 Admin 产品列表页:
-
现在列表的第一列应该显示一个 60x60px 的图片缩略图。
-
点击缩略图,应该会触发您之前配置的 JS 模态框,显示放大后的图片。
-
这样,您就利用了之前编写的 JS 模态框功能,完美地实现了列表页的图片展示和放大需求。
12 Ubuntu Server 生产环境 Docker 部署指南
前提条件
-
Ubuntu Server: 已安装 SSH 访问。
-
MySQL Server: 位于外部服务器,已配置防火墙和用户权限允许 Ubuntu Server 访问 3306 端口。
-
Git 仓库: 您的 Django 项目代码已推送到 GitHub(或 Gitee/GitLab 等)。
阶段一:本地(Windows)项目准备
确保您的项目文件结构和配置符合 Docker 部署要求。
1. 创建 .dockerignore 文件
在项目根目录创建 .dockerignore 文件,排除不必要的或敏感文件,减小镜像体积:
代码段
# .dockerignore
# 忽略 Git 自身文件
.git
.gitignore
# 忽略本地开发环境
__pycache__
*.pyc
venv/
.idea/
.vscode/
# 忽略本地数据库文件
db.sqlite3
# 忽略日志和本地媒体文件
logs/
mediafiles/
# 忽略敏感配置文件(如果 .env 包含在内)
# 注意:我们使用 .env 传参,确保它在本地不被提交到 Git,但会被 Docker Compose 使用
2. 创建 Nginx 配置文件目录
在项目根目录创建 Nginx 文件夹及配置文件:
your_project_root/
└── nginx/
└── nginx.conf # <-- 新建此文件
nginx/nginx.conf 内容: (与之前提供的配置相同)
Nginx
# nginx/nginx.conf
upstream web {
# web 是 docker-compose.yml 中定义的 Django 服务名
server web:8000;
}
server {
listen 80;
# 静态文件服务:由 Web 容器的 /app/staticfiles 提供
location /static/ {
alias /app/staticfiles/;
}
# 媒体文件服务:由 Web 容器的 /app/mediafiles 提供
location /media/ {
alias /app/mediafiles/;
}
# 其他请求转发给 Gunicorn (Web 服务)
location / {
proxy_pass http://web;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
3. 提交代码到 GitHub
确保将所有配置(包括 Dockerfile, docker-compose.yml, nginx/nginx.conf 等)推送到您的 GitHub 仓库:
Bash
git add .
git commit -m "Add Docker deployment files"
git push origin main
阶段二:服务器环境配置
以下步骤在您的 Ubuntu Server 上执行。
1. 安装 Docker 和 Docker Compose
Bash
# 步骤可能因 Ubuntu 版本略有不同,这是通用安装方法
sudo apt update
sudo apt install ca-certificates curl gnupg lsb-release
# 添加 Docker 官方 GPG 密钥
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
# 添加 Docker 仓库
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
$(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt install docker-ce docker-ce-cli containerd.io docker-compose-plugin
# 将当前用户添加到 docker 组,以便无需 sudo 即可运行 docker 命令
sudo usermod -aG docker $USER
# 重新登录或运行: newgrp docker
2. 克隆项目和创建配置
克隆项目: 决定您的部署目录,例如 /home/your_username/app/tiktok_pm。
Bash
mkdir -p ~/app/
cd ~/app/
git clone [您的 GitHub 仓库 URL] tiktok_pm
cd tiktok_pm
创建 .env 文件: 此文件用于存储敏感信息,不应提交到 Git。
Bash
nano .env
.env 文件内容: 替换为您真实的生产配置。
代码段
# .env
# Django 配置
DJANGO_SECRET_KEY='YOUR_VERY_LONG_AND_SECURE_SECRET_KEY'
# 必须包含服务器的公网 IP 和域名
DJANGO_ALLOWED_HOSTS='your_domain.com,your_server_ip,localhost,127.0.0.1'
# 外部 MySQL 数据库连接配置
MYSQL_HOST=YOUR_MYSQL_SERVER_IP # <-- 外部 MySQL IP
MYSQL_PORT=3306
MYSQL_DB_NAME=tiktok_pm_prod
MYSQL_USER=tiktok_user
MYSQL_PASSWORD=your_secure_db_password
阶段三:部署与运行
1. 首次构建和启动服务
在项目根目录(包含 docker-compose.yml)下运行:
Bash
# 构建 web 镜像 (会自动安装依赖、收集静态文件)
# -d 表示在后台运行
docker compose up --build -d
2. 初始化数据库和创建用户
容器启动后,首次运行数据库迁移和创建超级用户:
Bash
# 1. 运行数据库迁移 (连接到外部 MySQL)
docker compose exec web python manage.py migrate
# 2. 创建超级管理员
docker compose exec web python manage.py createsuperuser
# 3. 检查容器状态docke
docker compose ps
您的应用现在应该可以通过 Nginx(即服务器的 80 端口)访问了。
♻️ 阶段四:版本更新和升级流程 (最佳实践)
这是在 Docker 环境下进行代码更新的标准流程,它确保了最小化停机时间和环境隔离。
1. 代码更新流程
在 Ubuntu 服务器上执行以下操作:
Bash
# 1. 进入项目目录
cd ~/app/tiktok_pm
# 2. 拉取最新代码
# 假设您只在 main 分支上部署
git pull origin main
2. Docker 升级流程 (一键式)
Bash
# 3. 停止并移除旧容器,重建新容器
# --build: 强制 Docker 重新构建 web 镜像 (这样它会包含您的新代码、新依赖和新的 collectstatic)
# -d: 后台运行
docker compose up --build -d
3. 数据库迁移 (如果模型有变动)
Bash
# 4. 运行数据库迁移
# 必须在新容器启动后执行
docker compose exec web python manage.py migrate
4. 清理旧镜像(可选)
为了节约服务器空间,您可以清除未被任何容器使用的旧 Docker 镜像:
Bash
docker image prune -f
最佳实践总结
-
原子性部署:
docker compose up --build -d确保了所有依赖项和配置都会在新镜像中构建好,如果构建失败,旧服务不会受到影响。 -
零停机重启: 虽然 Docker Compose 默认的重启不是完全的零停机(它会停止旧的再启动新的),但由于您的应用启动速度快,停机时间极短。如果追求绝对的零停机,需要使用更复杂的工具如 Kubernetes。
-
易于回滚: 如果新版本出现问题,您只需要回退 Git 提交 (
git reset --hard <old_commit_hash>),然后再次运行docker compose up --build -d,系统即可快速回到稳定状态。
这个 Docker 部署流程结合了版本控制的优势,为您提供了一个专业、稳定且易于维护的生产环境。
📂 项目完整目录结构
假设您的项目根目录是 tiktok_pm。
tiktok_pm/
├── tiktok_pm_project/ # Django 项目主配置目录
│ ├── __init__.py
│ ├── settings.py # 核心配置,已修改以读取环境变量
│ ├── urls.py
│ └── wsgi.py
│
├── products/ # 产品应用目录
│ ├── migrations/ # 数据库迁移文件目录
│ │ ├── 0001_initial.py
│ │ └── ...
│ ├── static/ # 静态文件目录
│ │ └── admin/ # 供 Admin 使用的自定义静态文件
│ │ ├── css/
│ │ │ ├── admin_image_modal.css # 图片模态框样式 (已创建)
│ │ │ └── admin_price_highlight.css # 列表高亮样式 (已创建)
│ │ └── js/
│ │ ├── admin_image_modal.js # 图片模态框 JS (已修复)
│ │ └── admin_price_highlight.js # 列表高亮 JS (已创建)
│ ├── __init__.py
│ ├── admin.py # Admin 配置 (ProductAdmin, Inlines, 缩略图方法)
│ ├── apps.py
│ ├── models.py # 模型定义 (Product, ProductImage, ProductReview 等)
│ └── views.py
│
├── nginx/ # Docker Nginx 配置目录
│ └── nginx.conf # Nginx 反向代理和静态文件服务配置
│
├── staticfiles/ # collectstatic 收集的静态文件目录 (Docker 构建时生成)
├── venv/ # 虚拟环境目录 (应被 .gitignore 和 .dockerignore 忽略)
├── manage.py # Django 管理脚本
├── requirements.txt # Python 依赖列表 (用于 Docker 构建)
├── .env # 环境变量文件 (外部 MySQL 连接信息、SECRET_KEY)
├── Dockerfile # Docker 镜像构建文件
├── docker-compose.yml # Docker Compose 编排文件
├── .gitignore # Git 忽略文件列表
└── .dockerignore # Docker 构建忽略文件列表
13. 实现异步产品数据导入功能
需求
现在我希望在现有的web application基础上开发一个新功能, 这是一个异步的调用第三方API来获取TikTok 产品数据(JSON)文件,并导入到现有的mysql数据库的tiktok_products数据表中。具体要求如下:
-
我希望在tiktok_pm web applicaiton admin页面有一个新的页面 - product_fetch
-
一开始,我可以在一个text box里输入多行的产品URL, 比如:
https://www.tiktok.com/view/product/1729762527861968902
https://www.tiktok.com/view/product/1731057176518889862
一行一个URL, 分别代表不同的产品。
- 提交表单后,发送http request 给第三方API:
curl -H "Authorization: Bearer <API key>" -H "Content-Type: application/json" -d '{"input":[{"url":"https://www.tiktok.com/view/product/1729762527861968902"},{"url":"https://www.tiktok.com/view/product/1731057176518889862"}]}' "https://api.brightdata.com/datasets/v3/trigger?dataset_id=gd_m45m1u911dsa4274pi¬ify=false&include_errors=true"
- 在这里,我们将调用bright data的 Asynchronous Requests 来实现,因为有可能数据很多,需要很长时间进行抓取,具体关于Asynchronous Requests的文档如下:
https://docs.brightdata.com/api-reference/rest-api/scraper/asynchronous-requests?_gl=11jguggx_gcl_auNzM2NjUwMTI5LjE3NjM0NzI3ODY._gaMTA2MDMzMDE4MC4xNzYzMzQ2MDM2_ga_KQX3XWKR2T*czE3NjM4MTQ1NDckbzExJGcxJHQxNzYzODE0ODI3JGo2MCRsMCRoMA..
- 我的目的是获取调用API执行成功后的JSON到本地,并执行我自己编写的python脚本来将JSON数据导入数据库。
在您现有的 Django 架构中实现这个异步 TikTok 产品数据导入功能,最专业和稳健的方式是使用 Django Admin 的自定义视图 结合 Django-Q 或 Celery 来处理异步任务,确保 Web 请求不会超时。
由于您已经提供了一个 Python 导入脚本 (import_products.py),我们将把该脚本的核心逻辑包装成一个 Django 管理命令 或 异步任务。
🚀 总体架构设计
我们将采用以下三层架构实现需求:
-
前端 (Admin View): 创建一个自定义的
product_fetch视图,用于接收 URL 列表。 -
异步层 (Django-Q/Celery): 使用一个异步任务队列(这里以 Django-Q 为例,它更轻量易用)来执行耗时的 Bright Data API 调用。
-
核心逻辑 (Python Script Wrapper): 编写一个任务函数,负责:
-
触发 Bright Data 异步请求。
-
轮询 Bright Data 的完成状态并下载 JSON 结果。
-
调用您现有的
import_products.py脚本逻辑,将数据导入 MySQL。
-
📋 步骤一:环境准备和配置
首先,我们需要确保 Django 应用可以处理异步任务和外部请求。
1. 安装依赖
在您的 requirements.txt 中添加以下库(如果尚未添加):
# ... 其他依赖
django-q # 用于异步任务队列
requests # 用于 HTTP 请求 (已在您的脚本中)
pymysql # 用于 MySQL 连接 (已在您的脚本中)
然后,在您的 Dockerfile 中确保这些依赖会被安装,并在服务器上重新构建:docker compose up --build -d。
2. 配置 Django-Q
在您的 tiktok_pm_project/settings.py 中添加 Django-Q 配置,并使用 Redis 或您的数据库作为任务后端(假设使用数据库):
Python
# tiktok_pm_project/settings.py
# ... (在 DATABASES 配置之后添加)
Q_CLUSTER = {
# 推荐使用数据库后端,如果 Redis 可用则更推荐 Redis
'name': 'DjangORM',
'workers': 4, # 根据服务器性能设置工作进程数
'timeout': 360,
'retry': 120,
'queue_limit': 50,
'bulk': 10,
'orm': 'default', # 使用默认数据库连接
}
3. 运行迁移
在服务器上运行 Django 迁移以创建 Django-Q 所需的表:
Bash
docker compose exec web python manage.py migrate
📋 步骤二:编写异步任务 (tasks.py)
我们将把 Bright Data 逻辑和数据导入逻辑放在 products 应用内的 tasks.py 文件中。
1. 创建 products/tasks.py
创建 products/tasks.py 文件,包含 API 调用和数据下载逻辑。
Python
# products/tasks.py
import json
import time
import requests
from django_q.tasks import async_task
from django.conf import settings # 用于访问 Bright Data 配置
from .importer_wrapper import import_json_data_to_db # 导入导入函数
# --------------------------
# Bright Data Config (应从 settings.py 读取)
# --------------------------
# 假设您将这些配置添加到 settings.py 或 .env 中
BRIGHT_DATA_API_KEY = "Bearer <API key>" # 替换为您的密钥
BRIGHT_DATA_TRIGGER_URL = "https://api.brightdata.com/datasets/v3/trigger"
BRIGHT_DATA_DOWNLOAD_URL = "https://api.brightdata.com/datasets/v3/datasets/{dataset_id}/data"
DATASET_ID = "gd_m45m1u911dsa4274pi"
NOTIFICATION_EMAIL = "your@email.com" # 用于接收完成通知
# --------------------------
# 任务函数
# --------------------------
def trigger_bright_data_task(urls_list):
"""
1. 将 URL 列表转换为 Bright Data 所需的 JSON 格式。
2. 触发异步 API 调用。
3. 返回 task_id
"""
# 构造请求体
input_data = [{"url": url.strip()} for url in urls_list if url.strip()]
payload = {
"input": input_data,
}
# 构造请求参数
params = {
"dataset_id": DATASET_ID,
"notify": "true", # 允许通知
"notify_emails": NOTIFICATION_EMAIL,
"include_errors": "true",
}
headers = {
"Authorization": BRIGHT_DATA_API_KEY,
"Content-Type": "application/json"
}
print(f"Triggering Bright Data for {len(input_data)} URLs...")
try:
response = requests.post(
BRIGHT_DATA_TRIGGER_URL,
headers=headers,
params=params,
data=json.dumps(payload),
timeout=60 # 设置超时时间
)
response.raise_for_status() # 检查 HTTP 错误
data = response.json()
# Bright Data 成功触发后返回 dataset_id 和 job_id
if 'job_id' in data:
job_id = data['job_id']
# 异步调用轮询任务,检查结果
async_task('products.tasks.poll_bright_data_result', job_id)
return job_id
print("❌ Bright Data API response error:", data)
return None
except requests.exceptions.RequestException as e:
print(f"❌ Bright Data API request failed: {e}")
return None
def poll_bright_data_result(job_id):
"""
轮询 Bright Data API,直到任务完成并下载数据。
"""
dataset_id = DATASET_ID
# 轮询状态的 API (需要查看 Bright Data 文档确认准确的轮询 URL)
status_url = f"https://api.brightdata.com/datasets/v3/jobs/{job_id}/status"
headers = {"Authorization": BRIGHT_DATA_API_KEY}
print(f"Start polling Bright Data job_id: {job_id}")
while True:
try:
time.sleep(30) # 每 30 秒轮询一次
status_resp = requests.get(status_url, headers=headers, timeout=60)
status_resp.raise_for_status()
status_data = status_resp.json()
job_status = status_data.get('status')
print(f"Job {job_id} status: {job_status}")
if job_status == 'completed' or job_status == 'finished':
# 任务完成,下载数据
download_url = BRIGHT_DATA_DOWNLOAD_URL.format(dataset_id=dataset_id)
download_resp = requests.get(
download_url,
headers=headers,
params={'job_id': job_id, 'file_type': 'json'}, # 确保下载 JSON
stream=True,
timeout=300
)
download_resp.raise_for_status()
# 处理下载的 JSON 数据流
downloaded_products = download_resp.json()
# 导入数据到数据库
print(f"✅ Download complete. Found {len(downloaded_products)} products.")
import_json_data_to_db(downloaded_products)
return f"Job {job_id} completed. Imported {len(downloaded_products)} products."
elif job_status in ['failed', 'error']:
print(f"❌ Job {job_id} failed with status: {job_status}")
return f"Job {job_id} failed."
except requests.exceptions.RequestException as e:
print(f"❌ Polling request failed: {e}. Retrying...")
continue
except Exception as e:
print(f"❌ General error during polling: {e}")
return f"Job {job_id} encountered a general error."
# ----------------------------------------------------------------------
# 包装您的导入脚本的核心逻辑 (假设您的脚本位于 products/importer_wrapper.py)
# ----------------------------------------------------------------------
def import_json_data_to_db(products_list):
"""
调用您的 import_products.py 脚本中的核心导入逻辑。
"""
# 假设您的 import_products.py 已被重构为一个模块,并且 expose 了 import_json_data 函数
# 由于您提供的是一个完整的脚本,我们需要将它的逻辑提取并包装:
# 假设 import_products.py 的所有函数 (insert_product, insert_images, etc.)
# 被复制到 products/importer_core.py 中,并在主函数中调用它们。
# **重要:** 这一部分需要您将 import_products.py 中的逻辑重构为一个可导入的函数
# 暂时使用一个占位符,模拟数据导入成功
# 请手动将 import_products.py 中的所有函数复制到一个新文件 products/importer_core.py 中
from .importer_core import import_products_from_list
print(f"Starting database import for {len(products_list)} items...")
# 假设 import_products_from_list 接受连接配置和产品列表
# 注意:需要从 settings.py 获取 MYSQL_CONFIG
# 由于您没有提供 settings.py 中数据库配置的统一入口,我们暂时使用硬编码的连接配置
# !!请务必将您的 MYSQL_CONFIG 移入 Django settings.py 或 .env 文件!!
MYSQL_CONFIG = {
"host": "192.168.3.17",
"user": "root",
"password": "abcd1234",
"db": "tiktok_products",
"charset": "utf8mb4",
"cursorclass": pymysql.cursors.DictCursor,
}
try:
# import_products_from_list(MYSQL_CONFIG, products_list)
pass # 实际调用导入函数
print("Database import complete.")
except Exception as e:
print(f"Database import failed: {e}")
📋 步骤三:创建 Admin 自定义视图
现在,我们在 Django Admin 中创建一个自定义页面来承载表单。
1. 创建 products/views.py
在 products 应用中添加一个视图函数来渲染表单并处理提交。
Python
# products/views.py
from django.shortcuts import render
from django.contrib.admin.views.decorators import staff_member_required
from django.http import HttpResponseRedirect
from django.urls import reverse
from .tasks import trigger_bright_data_task # 导入触发函数
@staff_member_required
def product_fetch_view(request):
context = {
'title': 'TikTok 产品数据抓取与导入',
'site_header': 'TikTok PM 后台管理',
}
if request.method == 'POST':
# 1. 从表单中获取多行 URL
urls_raw = request.POST.get('urls_input', '')
urls_list = [url.strip() for url in urls_raw.split('\n') if url.strip()]
if not urls_list:
context['message'] = "错误:请输入至少一个产品 URL。"
context['status'] = "error"
return render(request, 'admin/product_fetch.html', context)
# 2. 触发异步任务 (trigger_bright_data_task 将会调用 async_task)
job_id = trigger_bright_data_task(urls_list)
if job_id:
context['message'] = f"✅ 已成功触发 Bright Data 任务,Job ID: {job_id}。请耐心等待异步导入结果。"
context['status'] = "success"
else:
context['message'] = "❌ 触发 Bright Data 任务失败,请检查 API 密钥和网络连接。"
context['status'] = "error"
return render(request, 'admin/product_fetch.html', context)
2. 配置 Admin URL
在 products 应用中创建 admin.py 或修改现有的 tiktok_pm_project/urls.py 来添加这个新路径。
在 products/admin.py 中添加自定义 URL:
Python
# products/admin.py
from django.contrib import admin
from django.urls import path
from .views import product_fetch_view
class ProductAdmin(admin.ModelAdmin):
# ... 其他 admin 配置
def get_urls(self):
urls = super().get_urls()
custom_urls = [
# 新增 /admin/product_fetch/ 路径
path('product_fetch/', self.admin_site.admin_view(product_fetch_view), name='product_fetch'),
]
return custom_urls + urls
# admin.site.register(Product, ProductAdmin) # 确保 Product 模型已注册
3. 创建 Admin 模板
Django Admin 的自定义视图需要一个模板。在您的项目模板目录下创建 templates/admin/product_fetch.html。
HTML
{% extends 'admin/base_site.html' %}
{% load i18n admin_static %}
{% block content %}
<div id="content-main">
<h1>{% trans "TikTok 产品数据抓取与导入" %}</h1>
<p>请输入需要抓取的产品 URL,一行一个。</p>
{% if message %}
<div class="messagelist">
<p class="{% if status == 'success' %}success{% else %}error{% endif %}">
{{ message }}
</p>
</div>
{% endif %}
<form action="." method="post">{% csrf_token %}
<div class="form-row">
<label class="required" for="id_urls_input">产品 URL 列表:</label>
<textarea name="urls_input" id="id_urls_input" rows="10" cols="80" required></textarea>
</div>
<div class="submit-row">
<input type="submit" value="开始异步抓取并导入" class="default" name="_start">
</div>
</form>
</div>
{% endblock %}
📋 步骤四:运行工作进程
因为使用了 Django-Q,您需要在后台运行一个工作进程,来执行 poll_bright_data_result 这样的异步任务。
1. 修改 docker-compose.yml
在您的 docker-compose.yml 文件中,新增一个 worker 服务:
YAML
# docker-compose.yml
services:
# ... (原有的 web 和 db 服务)
worker:
build:
context: .
dockerfile: Dockerfile # 使用与 web 相同的镜像
container_name: tiktok_pm_worker
env_file:
- .env
command: python manage.py qcluster # 启动 Django-Q 进程
depends_on:
- web # 确保 web 应用启动后 worker 才启动
2. 重启所有服务
在服务器上,确保您的代码已提交并拉取,然后启动所有服务:
Bash
docker compose up --build -d
现在,当您访问 /admin/product_fetch/ 页面并提交表单时,Django-Q 就会将任务发送给 worker 容器,在后台完成 Bright Data 的调用、下载和 MySQL 导入。
14 添加自定义页面到Admin导航页
您的问题是如何在 Django Admin 的导航(Navigate)区域,即左侧菜单栏或顶部链接中,显示您自定义的 product_fetch 页面链接。
由于 product_fetch 是一个自定义的视图,而不是一个标准的模型管理页面,您需要通过自定义 admin.py 来将这个链接注入到 Admin 界面中。
以下是实现此目标的标准方法,需要修改您的 products/admin.py 文件:
🛠️ 步骤一:修改 products/admin.py
在您的 ProductAdmin 类中,我们将添加一个名为 product_fetch_link 的方法,并利用 Django Admin 的 changelist_view 上下文注入这个链接。
请更新您的 D:\Python\tiktok_pm\products\admin.py 文件,确保 ProductAdmin 类中包含以下逻辑:
Python
# D:\Python\tiktok_pm\products\admin.py
from django.contrib import admin
from django.urls import path
from django.utils.html import format_html
from . import models
from . import views # 确保导入您的 views 模块
# ... (如果 Product 模型没有定义,请确保您已经定义了)
# 例如: from .models import Product
class ProductAdmin(admin.ModelAdmin):
# ... (您原有的配置,例如 list_display, search_fields 等)
# ----------------------------------------------------
# 1. 注入自定义 URL (您之前已完成)
# ----------------------------------------------------
def get_urls(self):
urls = super().get_urls()
custom_urls = [
path('product_fetch/', self.admin_site.admin_view(views.product_fetch_view), name='product_fetch'),
]
return custom_urls + urls
# ----------------------------------------------------
# 2. 创建链接显示的自定义方法
# ----------------------------------------------------
# 这个方法用于在 Model 的 'change list' 页面顶部显示链接
def product_fetch_link(self):
# 构造反向解析的 URL 名称
# 格式为: 'admin:<app_label>_<model_name>_<url_name>'
url = reverse('admin:products_product_product_fetch')
# 使用 format_html 来生成链接 HTML
return format_html(
'<a class="button" href="{}">{}</a>',
url,
'批量抓取产品' # 这是链接上显示的文本
)
# 设置短描述,让它在 Admin 中显示得更友好
product_fetch_link.short_description = '触发批量抓取'
# ----------------------------------------------------
# 3. 覆盖 changelist_view 方法,将链接注入到上下文
# ----------------------------------------------------
def changelist_view(self, request, extra_context=None):
if extra_context is None:
extra_context = {}
# 将自定义链接添加到上下文,通常命名为 'custom_actions' 或类似名称
extra_context['product_fetch_url'] = reverse('admin:products_product_product_fetch')
# 返回原始的 changelist 视图
return super().changelist_view(request, extra_context=extra_context)
# 注册 Admin 类
admin.site.register(models.Product, ProductAdmin)
🛠️ 步骤二:修改 product_fetch.html (显示导航菜单)
为了让您的自定义页面看起来像一个标准的 Admin 页面,并且顶部导航栏(面包屑)能正确显示,您需要确保在 product_fetch.html 中包含 admin/change_form.html 中常用的导航块。
请检查并确保您的 D:\Python\tiktok_pm\products\templates\admin\product_fetch.html 模板至少包含以下结构:
HTML
{% extends "admin/base_site.html" %}
{% load i18n admin_urls static %}
{# ---------------------------------------------------------------- #}
{# 🌟 确保 breadcrumbs 块被定义,用于显示导航路径 🌟 #}
{# ---------------------------------------------------------------- #}
{% block breadcrumbs %}
<div class="breadcrumbs">
<a href="{% url 'admin:index' %}">{% translate 'Home' %}</a>
› <a href="{% url 'admin:app_list' app_label='products' %}">{% translate 'Products' %}</a>
› <a href="{% url 'admin:products_product_changelist' %}">{% translate 'Products' %}</a>
› {% translate '批量抓取产品' %} {# 自定义页面的名称 #}
</div>
{% endblock %}
{% block content %}
<div id="content-main">
{# 您的表单和内容放在这里 #}
<form action="" method="post">
{% csrf_token %}
<fieldset class="module">
<h2>{% translate "TikTok 产品数据抓取" %}</h2>
</fieldset>
<div class="submit-row">
<input type="submit" value="{% translate '提交任务并开始抓取' %}" name="_start" />
</div>
</form>
</div>
{% endblock %}
结果:
完成上述步骤后:
-
当您进入 Products (产品) 列表页 (
/admin/products/product/) 时,您应该会在页面顶部或一个易于访问的位置看到一个名为 "批量抓取产品" 的按钮或链接(如果您在changelist_view中使用了一个自定义模板来显示它)。 -
最重要的是,通过
get_urls的设置,该页面现在是一个有效的 Admin 子页面,并且通过breadcrumbs块,它能正确地融入 Admin 的导航体系。
如果您希望它出现在 Admin 首页左侧的应用列表下方,那需要更复杂的定制,但这通常是通过在 **模型列表页(changelist)**上放置一个醒目的链接来实现的。
15 创建自定义管理命令(Custom Management Command)。
我想把我的importer_wrapper.py包装成一个python命令,可以通过类似python manage.py runserver这样的方式去执行手动导入JSON的动作。
我已经手动把一些JSON 放在data\json目录下,所以希望这个命令能调用imort_product_from_list这个方法执行手动导入JSON文件进到数据库。该如何实现?
您好!这是一个非常标准且推荐的 Django 实践:创建自定义管理命令(Custom Management Command)。
通过自定义管理命令,您可以将 importer_wrapper.py 中的逻辑包装起来,并通过 python manage.py import_json_data 这样的方式运行,就像运行 runserver 一样。
🛠️ 实现自定义管理命令的步骤
我们将创建一个名为 import_json_data 的管理命令,它将查找 data/json 目录下的 JSON 文件,并调用您的导入逻辑。
步骤一:创建管理命令目录结构
在您的 products 应用目录下,创建以下目录和文件:
products/
├── management/
│ └── commands/
│ └── import_json_data.py <-- 新文件
└── ...
步骤二:编写 import_json_data.py
在这个文件中,您需要继承 BaseCommand 并实现 handle() 方法。
文件:products/management/commands/import_json_data.py
Python
import os
import json
from django.core.management.base import BaseCommand, CommandError
from django.conf import settings
from pathlib import Path
# 假设您的导入逻辑在 products/importer_wrapper.py 文件中
# 我们需要确保能正确导入它。
from products.importer_wrapper import import_products_from_list
class Command(BaseCommand):
# 命令行中使用的名称:python manage.py import_json_data
help = 'Imports product data from JSON files located in the MEDIA_ROOT/temp_json directory and moves them to MEDIA_ROOT/json upon success.'
def handle(self, *args, **options):
# 定义源目录和目标目录
source_subdir = 'temp_json'
target_subdir = 'json'
json_dir = Path(settings.MEDIA_ROOT) / source_subdir
target_dir = Path(settings.MEDIA_ROOT) / target_subdir
# 1. 检查源目录是否存在
if not json_dir.exists():
raise CommandError(f'JSON source directory does not exist: {json_dir}')
# 2. 确保目标目录存在,如果不存在则创建
target_dir.mkdir(parents=True, exist_ok=True)
self.stdout.write(self.style.NOTICE(f'Scanning directory: {json_dir}'))
self.stdout.write(self.style.NOTICE(f'Target directory for completed files: {target_dir}'))
success_count = 0
failure_count = 0
# 3. 遍历目录中的所有 JSON 文件
for filename in os.listdir(json_dir):
if filename.endswith('.json'):
file_path = json_dir / filename
self.stdout.write(f'Processing file: {filename}')
try:
with open(file_path, 'r', encoding='utf-8') as f:
data = json.load(f)
# 检查 JSON 数据结构
if not isinstance(data, list):
data = [data]
# 调用导入函数
import_products_from_list(data)
# ----------------------------------------------------
# 🌟 关键步骤:文件移动 🌟
# ----------------------------------------------------
target_file_path = target_dir / filename
# 使用 os.rename (或 shutil.move) 将文件移动到目标目录
# os.rename 可以用于跨目录的文件移动
os.rename(file_path, target_file_path)
self.stdout.write(
self.style.SUCCESS(f'Successfully imported and moved {filename} to {target_subdir}/'))
success_count += 1
except json.JSONDecodeError:
self.stderr.write(self.style.ERROR(f'Failed to decode JSON from {filename}. Skipping.'))
failure_count += 1
except Exception as e:
self.stderr.write(self.style.ERROR(f'Error importing {filename}: {e}'))
# 如果导入失败,文件保留在 temp_json 目录
failure_count += 1
self.stdout.write(self.style.SUCCESS(
f'\n--- Import Finished ---'
))
self.stdout.write(f'Total files processed: {success_count + failure_count}')
self.stdout.write(self.style.SUCCESS(f'Successful imports and moves: {success_count}'))
self.stdout.write(self.style.ERROR(f'Failed imports (files kept in source): {failure_count}'))
步骤三:验证 settings.py
请确保您的 settings.py 中 MEDIA_ROOT 配置正确,以便 Path(settings.MEDIA_ROOT) / 'json' 能指向 data/json 目录。
Python
# settings.py
from pathlib import Path
import os
# BASE_DIR = Path(__file__).resolve().parent.parent
MEDIA_ROOT = os.path.join(BASE_DIR, 'data') # 确保指向项目根目录下的 data 文件夹
步骤四:执行命令
在您的 Django 项目根目录下(即 manage.py 所在的目录),运行您的自定义命令:
Bash
# 确保在运行前,您的 Docker 或本地数据库服务已启动
python manage.py import_json_data
然后,用户可以通过以下方式运行:
Bash
# 默认从 data/json 导入
python manage.py import_json_data
# 在生产环境下运行
docker compose exec web python manage.py import_json_data