--- aliases: cssclasses: tags: - "#django" - "#python" - "#mysql" kanban-plugin: title: source: author: published: created: description: link: --- #django #python #mysql #mariadb #project #tiktok ```table-of-contents 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 项目 ### 一期规划 - [x] 如果数据从JSON导入,那么 source ID不为空,在admin页面里source id需要变成read-only不可编辑 ✅ 2025-11-21 - [x] description, description1, description2 这些字段需要用富文本框进行编辑和显示 ✅ 2025-11-24 - [x] 在product image和product variation section, 能根据zipline_url直接显示图片缩略图 ✅ 2025-11-21 - [x] 点击图片可以方法看原始尺寸 ✅ 2025-11-21 - [ ] 调整zipline_url编辑框尺寸 - [x] 缺少specification字段 ✅ 2025-11-21 - [ ] spcifications字段可以根据JSON内容,分别提供编辑框编辑JSON里每一个name,value字段, 默认折叠这个section不显示 - [x] created_at和updated_at都没有值,在没有使用ORM之前该字段没有长度限制,现在是6。如何修改可以让这两个字段自动带上日期时间戳? ✅ 2025-11-21 - [x] 修改过滤条件按,在product list页面可以根据store name进行过滤,去除现有的一些过滤条件比如available, in stock, created at etc. ✅ 2025-11-24 - [x] 增加prodcut_reviews table. ✅ 2025-11-21 - [x] 在product list页面可以批量删除product,包含关联的product_images, product_videos, product_variations, product_reviews ✅ 2025-11-21 - [ ] 在product list页面可以高亮final price在10~50之间的产品,整条记录高亮,以区别其他的产品,10~50这个区间可以通过过滤条件来修改? - [ ] 按不同颜色显示不同sold数量的商品 - [ ] 在base info section显示第一张product image - [x] product list页面,显示每个产品的第一张图片的缩略图, 点击可以打开放大。 ✅ 2025-11-21 - [x] 将整个项目push到Github个人账户下 ✅ 2025-11-22 - [x] 把下载的JSON存放在某个特定目录下 data\json ✅ 2025-11-24 - [x] 根据decription detail生成HTML文件存放在data\html下 ✅ 2025-11-24 - [x] 在产品详情页面增加了view html链接,可以直接打开descrtpion detail html ✅ 2025-11-24 - [ ] 继续优化product fetch页面,可以根据不同的抓取方式发送不同的request - [x] 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 - [x] 抓取数据默认设置一次性抓取上限值,放在settings里可以配置 - [ ] 用AI设计来分析抓取的数据,并在superset里实现 - [ ] 在products表里记录snapshot name这样可以回溯原始的JSON - [ ] 研究Djang可否直接把model导出成JSON(单个产品)以便于被n8n读取 ### 二期规划 - [ ] 通过淘宝开放平台拍立淘API来实现,图找商品的功能, 目前看起来浏览器插件可完成 - [x] 在应用里实现按产品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.py` 和 `products/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 ``` _(输入用户名、邮箱和密码。)_ ``` bash #superuser username = admin password = Abcd_1234 ``` ### 2.5. 运行项目 1. **启动开发服务器:** Bash ``` python manage.py runserver ``` 1. **访问 Admin 后台:** 在浏览器中打开 **`http://127.0.0.1:8000/admin/`**,使用您刚刚创建的超级管理员账户登录。 ## 3. 定义模型 ### 3.1:保存 `products/models.py` 文件 ``` python 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 `**:记录模型更改。 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`** 文件,并按以下步骤添加代码。 ``` python 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'') 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'') 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 ``` python # tiktok_pm_project/settings.py INSTALLED_APPS = [ # ... 其他应用 'rest_framework', # 富文本编辑器 'tinymce', # <-- 新增 'products', ] ``` ### 6.3: 修改 `ProductAdmin` 我们需要从 `tinymce` 导入专用的表单字段,并覆盖 `ProductAdmin` 的表单属性。 打开 **`products/admin.py`**,进行以下修改: Python ``` 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 ``` 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 ``` 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 ``` 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 ``` 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 ``` 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` 应用目录下创建静态文件所需的路径: ``` bash 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 ``` 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 ``` 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(`
×
`); 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` 方法,使其不再生成 `` 标签,而是生成带有 `data-large-url` 属性和 `image-clickable` class 的 `` 标签。 Python ``` 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: # 不再使用 标签,而是使用带 data 属性的 标签 return mark_safe(f''' ''') 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: # 不再使用 标签 return mark_safe(f''' ''') 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 python manage.py collectstatic ``` _(在开发环境中通常不需要,但这是一个好习惯。)_ 2. **启动服务器:** Bash ``` python python manage.py runserver ``` 3. **测试:** 登录 Admin 后台,进入产品编辑页面,点击产品图片预览。图片现在应该会在一个覆盖整个屏幕的模态框中放大显示。您可以点击右上角的 `×` 或按 `Esc` 键来关闭模态框。 这样,您就实现了简洁且用户体验更好的图片放大功能。 ## 9. 新增 `product_reviews` 表是一个关键的步骤,用于存储产品的评价数据。 我们将按照标准的 Django 流程来完成这个任务:**创建模型**、**配置关联**、**数据库迁移**,并将其集成到 **Admin 后台**。 ### 9.1 整体步骤概览 1. **`products/models.py`**: 定义新的 `ProductReview` 模型。 2. **数据库迁移**: 运行 `makemigrations` 和 `migrate` 创建新表。 3. **`products/admin.py`**: 注册 `ProductReview` 模型,并将其内联到 `Product` 编辑页。 --- ### 9.2: 创建 `ProductReview` 模型 打开您的 **`products/models.py`** 文件,添加以下 `ProductReview` 模型定义。 📄 `products/models.py` Python ``` 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 ``` bash python manage.py makemigrations products ``` ### 2. 应用迁移到数据库 Bash ``` bash python manage.py migrate ``` _(执行成功后,`product_reviews` 表将在数据库中创建。)_ --- ### 9.4: 配置 Admin 后台 我们将采用内联方式,让您可以在编辑 `Product` 时,直接查看和管理其关联的所有 `ProductReview`。 打开您的 **`products/admin.py`** 文件,进行以下修改: 📄 `products/admin.py` #### 1. 新增 `ProductReviewInline` 辅助类 在 `ProductImageInline` 等内联类定义的下方,添加 `ProductReviewInline`: Python ``` 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 ``` 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 解决方案:修改 `ProductAdmin` 的 `list_filter` 打开您的 **`products/admin.py`** 文件,找到 `ProductAdmin` 类的定义,并修改 `list_filter` 属性如下: Python ``` 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 ``` 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 ``` 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''' ''') 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 ``` 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` 文件,排除不必要的或敏感文件,减小镜像体积: 代码段 ``` bash # .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 ``` python # 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 ``` 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 `),然后再次运行 `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, 分别代表不同的产品。 3. 提交表单后,发送http request 给第三方API: ``` bash curl -H "Authorization: Bearer " -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" ``` 4. 在这里,我们将调用bright data的 Asynchronous Requests 来实现,因为有可能数据很多,需要很长时间进行抓取,具体关于Asynchronous Requests的文档如下: https://docs.brightdata.com/api-reference/rest-api/scraper/asynchronous-requests?_gl=1*1jguggx*_gcl_au*NzM2NjUwMTI5LjE3NjM0NzI3ODY.*_ga*MTA2MDMzMDE4MC4xNzYzMzQ2MDM2*_ga_KQX3XWKR2T*czE3NjM4MTQ1NDckbzExJGcxJHQxNzYzODE0ODI3JGo2MCRsMCRoMA.. 5. 我的目的是获取调用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 ``` 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 ``` 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 " # 替换为您的密钥 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 ``` 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 ``` 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 ``` html {% extends 'admin/base_site.html' %} {% load i18n admin_static %} {% block content %}

{% trans "TikTok 产品数据抓取与导入" %}

请输入需要抓取的产品 URL,一行一个。

{% if message %}

{{ message }}

{% endif %}
{% csrf_token %}
{% 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:__' url = reverse('admin:products_product_product_fetch') # 使用 format_html 来生成链接 HTML return format_html( '
{}', 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 %} {% endblock %} {% block content %}
{# 您的表单和内容放在这里 #}
{% csrf_token %}

{% translate "TikTok 产品数据抓取" %}

{% 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 ``` 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 ``` bash # 默认从 data/json 导入 python manage.py import_json_data # 在生产环境下运行 docker compose exec web python manage.py import_json_data ```