Files
nexus/raw/Others/TikTok PM - Python Django Project.md

94 KiB
Raw Blame History

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
mariadb
mysql
project
python
tiktok

#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在1050之间的产品整条记录高亮以区别其他的产品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. 项目规划

  1. 项目初始化: django-admin startproject tiktok_pm & python manage.py startapp products

  2. 配置数据库:settings.py 中配置 MySQL 连接。

  3. 定义模型:products/models.py 中编写 ORM 模型 (参考 2.1)。

  4. 运行迁移: python manage.py makemigrations & python manage.py migrate

  5. 注册 Admin:products/admin.py 中注册模型并定制搜索/过滤/列表展示。

  6. 安装 DRF: pip install djangorestframework

  7. 编写 API:products/serializers.pyproducts/views.py 中编写 DRF 组件。

  8. 部署: 使用 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 框架。

  1. 打开您的命令行工具 (如 Terminal, CMD, 或 PowerShell)。

  2. 创建并激活虚拟环境 (强烈推荐):

    Bash

    # 1. 创建名为 'venv' 的虚拟环境
    python -m venv venv
    
    # 2. 激活虚拟环境 (Windows)
    venv\Scripts\activate
    
    # 3. 激活虚拟环境 (macOS/Linux)
    source venv/bin/activate
    

    (激活后,您会在命令行前看到 (venv) 标识。)

  3. 安装核心依赖库:

    Bash

    pip install Django djangorestframework mysqlclient
    
    • Django: 核心 Web 框架。

    • djangorestframework: 用于构建 API 接口 (方便 n8n 集成)。

    • mysqlclient: Python 连接 MySQL 数据库的驱动。

2. 项目初始化与配置

Django 采用“项目 (Project)”和“应用 (App)”的两级结构。

2.1. 创建 Django 项目 (Project)

项目是整个应用的总配置和环境设置。

  1. 在命令行中执行以下命令创建项目:

    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)

应用是实现具体功能 (如产品管理) 的模块。

  1. 在命令行中创建应用:

    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. 首次迁移与创建管理员

  1. 应用配置的迁移:

    Bash

    python manage.py migrate
    

    (这会创建 Django 自身需要的用户、会话等基础表。)

  2. 创建超级管理员账户 (用于登录 Admin 后台):

    Bash

    python manage.py createsuperuser
    

    (输入用户名、邮箱和密码。)

    #superuser
    
    username = admin
    password = Abcd_1234
    

2.5. 运行项目

  1. 启动开发服务器:

    Bash

    python manage.py runserver
    
  2. 访问 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),都必须遵循这两步流程:

  1. makemigrations <app_name>:记录模型更改。

  2. 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. 模态框图片放大实现方案

我们需要创建两个文件:

  1. admin_image_modal.css: 定义模态框的样式。

  2. 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">&times;</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)

我们需要进行两处修改:

  1. ProductAdmin 类中添加 Media 内部类,引入新的 CSS 和 JS 文件。

  2. 修改 image_preview 方法,使其不再生成 <a> 标签,而是生成带有 data-large-url 属性和 image-clickable class 的 <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: 运行验证

  1. 收集静态文件: 由于您添加了新的静态文件,如果您的项目部署在生产环境中,需要运行:

    Bash

    python manage.py collectstatic
    

    (在开发环境中通常不需要,但这是一个好习惯。)

  2. 启动服务器:

    Bash

    python manage.py runserver
    
  3. 测试: 登录 Admin 后台,进入产品编辑页面,点击产品图片预览。图片现在应该会在一个覆盖整个屏幕的模态框中放大显示。您可以点击右上角的 &times; 或按 Esc 键来关闭模态框。

这样,您就实现了简洁且用户体验更好的图片放大功能。

9. 新增 product_reviews 表是一个关键的步骤,用于存储产品的评价数据。

我们将按照标准的 Django 流程来完成这个任务:创建模型配置关联数据库迁移,并将其集成到 Admin 后台

9.1 整体步骤概览

  1. products/models.py: 定义新的 ProductReview 模型。

  2. 数据库迁移: 运行 makemigrationsmigrate 创建新表。

  3. 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 运行验证

  1. 启动开发服务器: python manage.py runserver

  2. 访问 Admin 后台,进入任何一个产品的编辑页。

  3. 您应该能在页面底部看到一个新的可折叠的 "产品评价 (Product reviews)" 内联表单,您可以手动添加数据或查看通过导入工具创建的数据。

至此,您已成功将 product_reviews 表集成到您的 Django 项目中。

10 修改过滤条件

好的,这是一个清晰的管理界面定制要求。您希望在 Product 列表页的侧边栏过滤器中,用 store_name (店铺名称) 替换掉现有的 available, In_stock, 和 created_at 过滤条件。

这个修改只需要在 products/admin.py 文件中进行。

10.1 解决方案:修改 ProductAdminlist_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 列表页 显示第一张图片的缩略图并支持点击放大,我们需要做两处核心修改:

  1. products/admin.py 中:

    • ProductAdmin 类中添加一个方法,用于获取并渲染产品的第一张图片。

    • 将该方法添加到 list_display 列表中。

  2. 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' 添加到了您希望显示的位置(通常是第一列)。

运行验证

  1. 重启服务器:

    Bash

    python manage.py runserver
    
  2. 访问 Admin 产品列表页:

    • 现在列表的第一列应该显示一个 60x60px 的图片缩略图。

    • 点击缩略图,应该会触发您之前配置的 JS 模态框,显示放大后的图片。

这样,您就利用了之前编写的 JS 模态框功能,完美地实现了列表页的图片展示和放大需求。

12 Ubuntu Server 生产环境 Docker 部署指南

前提条件

  1. Ubuntu Server: 已安装 SSH 访问。

  2. MySQL Server: 位于外部服务器,已配置防火墙和用户权限允许 Ubuntu Server 访问 3306 端口。

  3. 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数据表中。具体要求如下

  1. 我希望在tiktok_pm web applicaiton admin页面有一个新的页面 - product_fetch

  2. 一开始我可以在一个text box里输入多行的产品URL, 比如:

https://www.tiktok.com/view/product/1729762527861968902

https://www.tiktok.com/view/product/1731057176518889862

一行一个URL 分别代表不同的产品。

  1. 提交表单后发送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&notify=false&include_errors=true"
  1. 在这里我们将调用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..

  1. 我的目的是获取调用API执行成功后的JSON到本地并执行我自己编写的python脚本来将JSON数据导入数据库。

在您现有的 Django 架构中实现这个异步 TikTok 产品数据导入功能,最专业和稳健的方式是使用 Django Admin 的自定义视图 结合 Django-Q 或 Celery 来处理异步任务,确保 Web 请求不会超时。

由于您已经提供了一个 Python 导入脚本 (import_products.py),我们将把该脚本的核心逻辑包装成一个 Django 管理命令异步任务

🚀 总体架构设计

我们将采用以下三层架构实现需求:

  1. 前端 (Admin View): 创建一个自定义的 product_fetch 视图,用于接收 URL 列表。

  2. 异步层 (Django-Q/Celery): 使用一个异步任务队列(这里以 Django-Q 为例,它更轻量易用)来执行耗时的 Bright Data API 调用。

  3. 核心逻辑 (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>
    &rsaquo; <a href="{% url 'admin:app_list' app_label='products' %}">{% translate 'Products' %}</a>
    &rsaquo; <a href="{% url 'admin:products_product_changelist' %}">{% translate 'Products' %}</a>
    &rsaquo; {% 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 %}

结果:

完成上述步骤后:

  1. 当您进入 Products (产品) 列表页 (/admin/products/product/) 时,您应该会在页面顶部或一个易于访问的位置看到一个名为 "批量抓取产品" 的按钮或链接(如果您在 changelist_view 中使用了一个自定义模板来显示它)。

  2. 最重要的是,通过 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