美多商城项目-05

SPUSKU

电商 / 商品管理系统里,SPUSKU 是两个非常核心、但经常被混淆的概念。你可以把它们理解为:“一类商品” vs “一个具体商品”

SPU(Standard Product Unit)标准化产品单元,表示“这是什么产品”

SKU(Stock Keeping Unit)库存量单位,表示“这个产品的具体规格”

SPU 是什么?

SPU = 一组具有相同核心属性的商品集合

它关注的是商品的共性信息,不区分具体规格。

典型包含:

  • 商品名称
  • 品牌
  • 商品描述
  • 类目
  • 统一的功能属性(不可变)

SPU 不直接参与库存、下单、价格计算

示例:iPhone 15 这就是一个 SPU,不管它是黑色、白色、128G 还是 256G,本质都是 iPhone 15

SKU 是什么?

SKU = 最小库存单位,是可以被下单、发货的商品

它关注的是具体差异,每个 SKU 都是独立存在的。

典型包含:

  • 规格属性(颜色 / 内存 / 尺码)
  • 唯一 SKU 编码
  • 库存数量
  • 售价
  • 条形码

SKU 才是真正参与库存管理和交易的对象

示例:

  • iPhone 15 · 黑色 · 128G
  • iPhone 15 · 白色 · 256G

每一个都是一个 SKU

商品案例

商品:T 恤

  • SPU:纯棉 T 恤
  • SKU:
    • 纯棉 T 恤 · 黑色 · M
    • 纯棉 T 恤 · 黑色 · L
    • 纯棉 T 恤 · 白色 · M

最终表结构

spu(商品表)
 ├── spec(规格表)
 └── sku(库存表)

SPU 表(商品主表)

描述“这是个什么商品”

CREATE TABLE spu (
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(100) NOT NULL COMMENT '商品名称',
    description VARCHAR(255) COMMENT '商品描述'
);

示例数据

INSERT INTO spu (name, description)
VALUES ('纯棉 T 恤', '100% 纯棉,舒适透气');

规格表(spec)

CREATE TABLE spec (
    id INT PRIMARY KEY AUTO_INCREMENT,
    spu_id INT NOT NULL COMMENT '所属SPU',
    name VARCHAR(50) NOT NULL COMMENT '规格名,如 颜色 / 尺码',
    FOREIGN KEY (spu_id) REFERENCES spu(id)
);

示例数据

INSERT INTO spec (spu_id, name) VALUES
(1, '颜色'),
(1, '尺码');

SKU 表(库存 + 交易)

SKU = 真正卖、真正扣库存的

CREATE TABLE sku (
    id INT PRIMARY KEY AUTO_INCREMENT,
    spu_id INT NOT NULL COMMENT '所属SPU',
    spec_values VARCHAR(100) NOT NULL COMMENT '规格值,如 黑色-M',
    price DECIMAL(10,2) NOT NULL COMMENT '价格',
    stock INT NOT NULL COMMENT '库存',
    FOREIGN KEY (spu_id) REFERENCES spu(id)
);

示例数据

INSERT INTO sku (spu_id, spec_values, price, stock) VALUES
(1, '黑色-M', 99.00, 100),
(1, '黑色-L', 99.00, 80),
(1, '白色-M', 89.00, 120);

商品页面讲解:

美多商城项目-05

model 代码完善

新建 goods app

cd apps 
python ../manage.py startapp goods

settings.py 注册 goods

INSTALLED_APPS = [
    ...
    'apps.goods',
]

完善apps/goods/urls.py

from django.urls import path
from . import views

app_name = "goods"

urlpatterns = [
    path('list/<int:category_id>/<int:page_num>/', views.ListView.as_view(), name='list'),
    path('detail/<int:sku_id>/', views.DetailView.as_view(), name='detail'),
]

新建apps/goods/utils.py

def get_breadcrumb(category):
    """
    根据分类获取面包屑
    """
    breadcrumb = {}

    # 三级分类
    if category.parent and category.parent.parent:
        breadcrumb['cat1'] = category.parent.parent
        breadcrumb['cat2'] = category.parent
        breadcrumb['cat3'] = category
    # 二级分类
    elif category.parent:
        breadcrumb['cat1'] = category.parent
        breadcrumb['cat2'] = category
    # 一级分类
    else:
        breadcrumb['cat1'] = category

    return breadcrumb

编辑 model

apps/goods/models.py

from django.db import models
from utils.models import BaseModel


class GoodsCategory(BaseModel):
    """商品类别"""
    name = models.CharField(max_length=10, verbose_name='名称')
    parent = models.ForeignKey(
        'self',
        related_name='subs',
        null=True,
        blank=True,
        on_delete=models.CASCADE,
        verbose_name='父类别'
    )

    class Meta:
        db_table = 'tb_goods_category'
        verbose_name = '商品类别'
        verbose_name_plural = verbose_name

    def __str__(self):
        return self.name


class GoodsChannelGroup(BaseModel):
    """商品频道组"""
    name = models.CharField(max_length=20, verbose_name='频道组名')

    class Meta:
        db_table = 'tb_channel_group'
        verbose_name = '商品频道组'
        verbose_name_plural = verbose_name

    def __str__(self):
        return self.name


class GoodsChannel(BaseModel):
    """商品频道"""
    group = models.ForeignKey(
        GoodsChannelGroup,
        on_delete=models.CASCADE,
        verbose_name='频道组名'
    )
    category = models.ForeignKey(
        GoodsCategory,
        on_delete=models.CASCADE,
        verbose_name='顶级商品类别'
    )
    url = models.CharField(max_length=50, verbose_name='频道页面链接')
    sequence = models.IntegerField(verbose_name='组内顺序')

    class Meta:
        db_table = 'tb_goods_channel'
        verbose_name = '商品频道'
        verbose_name_plural = verbose_name

    def __str__(self):
        return self.category.name


class Brand(BaseModel):
    """品牌"""
    name = models.CharField(max_length=20, verbose_name='名称')
    logo = models.ImageField(verbose_name='Logo图片')
    first_letter = models.CharField(max_length=1, verbose_name='品牌首字母')

    class Meta:
        db_table = 'tb_brand'
        verbose_name = '品牌'
        verbose_name_plural = verbose_name

    def __str__(self):
        return self.name


class SPU(BaseModel):
    """商品SPU"""
    name = models.CharField(max_length=50, verbose_name='名称')
    brand = models.ForeignKey(
        Brand,
        on_delete=models.PROTECT,
        verbose_name='品牌'
    )
    category1 = models.ForeignKey(
        GoodsCategory,
        on_delete=models.PROTECT,
        related_name='cat1_spu',
        verbose_name='一级类别'
    )
    category2 = models.ForeignKey(
        GoodsCategory,
        on_delete=models.PROTECT,
        related_name='cat2_spu',
        verbose_name='二级类别'
    )
    category3 = models.ForeignKey(
        GoodsCategory,
        on_delete=models.PROTECT,
        related_name='cat3_spu',
        verbose_name='三级类别'
    )
    sales = models.IntegerField(default=0, verbose_name='销量')
    comments = models.IntegerField(default=0, verbose_name='评价数')
    desc_detail = models.TextField(default='', verbose_name='详细介绍')
    desc_pack = models.TextField(default='', verbose_name='包装信息')
    desc_service = models.TextField(default='', verbose_name='售后服务')

    class Meta:
        db_table = 'tb_spu'
        verbose_name = '商品SPU'
        verbose_name_plural = verbose_name

    def __str__(self):
        return self.name


class SKU(BaseModel):
    """商品SKU"""
    name = models.CharField(max_length=50, verbose_name='名称')
    caption = models.CharField(max_length=100, verbose_name='副标题')
    spu = models.ForeignKey(
        SPU,
        on_delete=models.CASCADE,
        verbose_name='商品'
    )
    category = models.ForeignKey(
        GoodsCategory,
        on_delete=models.PROTECT,
        verbose_name='从属类别'
    )
    price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='单价')
    cost_price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='进价')
    market_price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='市场价')
    stock = models.IntegerField(default=0, verbose_name='库存')
    sales = models.IntegerField(default=0, verbose_name='销量')
    comments = models.IntegerField(default=0, verbose_name='评价数')
    is_launched = models.BooleanField(default=True, verbose_name='是否上架销售')
    default_image = models.ImageField(
        max_length=200,
        default='',
        null=True,
        blank=True,
        verbose_name='默认图片'
    )

    class Meta:
        db_table = 'tb_sku'
        verbose_name = '商品SKU'
        verbose_name_plural = verbose_name

    def __str__(self):
        return '%s: %s' % (self.id, self.name)


class SKUImage(BaseModel):
    """SKU图片"""
    sku = models.ForeignKey(
        SKU,
        on_delete=models.CASCADE,
        verbose_name='sku'
    )
    image = models.ImageField(verbose_name='图片')

    class Meta:
        db_table = 'tb_sku_image'
        verbose_name = 'SKU图片'
        verbose_name_plural = verbose_name

    def __str__(self):
        return '%s %s' % (self.sku.name, self.id)


class SPUSpecification(BaseModel):
    """商品SPU规格"""
    spu = models.ForeignKey(
        SPU,
        on_delete=models.CASCADE,
        related_name='specs',
        verbose_name='商品SPU'
    )
    name = models.CharField(max_length=20, verbose_name='规格名称')

    class Meta:
        db_table = 'tb_spu_specification'
        verbose_name = '商品SPU规格'
        verbose_name_plural = verbose_name

    def __str__(self):
        return '%s: %s' % (self.spu.name, self.name)


class SpecificationOption(BaseModel):
    """规格选项"""
    spec = models.ForeignKey(
        SPUSpecification,
        related_name='options',
        on_delete=models.CASCADE,
        verbose_name='规格'
    )
    value = models.CharField(max_length=20, verbose_name='选项值')

    class Meta:
        db_table = 'tb_specification_option'
        verbose_name = '规格选项'
        verbose_name_plural = verbose_name

    def __str__(self):
        return '%s - %s' % (self.spec, self.value)


class SKUSpecification(BaseModel):
    """SKU具体规格"""
    sku = models.ForeignKey(
        SKU,
        related_name='specs',
        on_delete=models.CASCADE,
        verbose_name='sku'
    )
    spec = models.ForeignKey(
        SPUSpecification,
        on_delete=models.PROTECT,
        verbose_name='规格名称'
    )
    option = models.ForeignKey(
        SpecificationOption,
        on_delete=models.PROTECT,
        verbose_name='规格值'
    )

    class Meta:
        db_table = 'tb_sku_specification'
        verbose_name = 'SKU规格'
        verbose_name_plural = verbose_name

    def __str__(self):
        return '%s: %s - %s' % (self.sku, self.spec.name, self.option.value)


class GoodsVisitCount(BaseModel):
    """统计分类商品访问量模型类"""
    category = models.ForeignKey(
        GoodsCategory,
        on_delete=models.CASCADE,
        verbose_name='商品分类'
    )
    count = models.IntegerField(default=0, verbose_name='访问量')
    date = models.DateField(auto_now_add=True, verbose_name='统计日期')

    class Meta:
        db_table = 'tb_goods_visit'
        verbose_name = '统计分类商品访问量'
        verbose_name_plural = verbose_name

修改apps/goods/apps.py:

from django.apps import AppConfig


class GoodsConfig(AppConfig):
    default_auto_field = "django.db.models.BigAutoField"
    name = "apps.goods"

完善apps/goods/views.py

from django.shortcuts import render
from django.views import View
import logging

from apps.goods.models import SKU, GoodsCategory
from apps.goods.utils import get_breadcrumb
from apps.contents.utils import get_categories

logger = logging.getLogger("django")


class ListView(View):
    """
    商品列表页
    /list/<category_id>/<page_num>/
    """

    def get(self, request, category_id, page_num):

        # 一、分类 & 面包屑
        try:
            category = GoodsCategory.objects.get(id=category_id)
        except GoodsCategory.DoesNotExist:
            return render(request, "list.html", {"errmsg": "分类不存在"})

        breadcrumb = get_breadcrumb(category)

        # 二、排序
        sort = request.GET.get("sort")
        if sort == "hot":
            order_field = "-sales"
        elif sort == "price":
            order_field = "price"
        else:
            order_field = "create_time"
            sort = "default"

        # 三、查询 SKU
        skus = SKU.objects.filter(
            category_id=category_id,
            is_launched=True
        ).order_by(order_field)

        # 四、分页
        try:
            page_num = int(page_num)
        except Exception:
            page_num = 1

        from django.core.paginator import Paginator
        paginator = Paginator(skus, 5)
        page_skus = paginator.page(page_num)
        total_page = paginator.num_pages

        # 五、上下文
        context = {
            "category": category,
            "categories": get_categories(),
            "breadcrumb": breadcrumb,
            "page_skus": page_skus,
            "sort": sort,
            "page_num": page_num,
            "total_page": total_page,
        }

        return render(request, "list.html", context)


class DetailView(View):
    """
    商品详情页
    /detail/<sku_id>/
    """

    def get(self, request, sku_id):
        try:
            sku = SKU.objects.get(id=sku_id)
        except SKU.DoesNotExist:
            return render(request, '404.html')

        categories = get_categories()
        breadcrumb = get_breadcrumb(sku.category)

        # 新增:SKU 图片
        sku_images = sku.skuimage_set.all()

        # ========= 规格处理 =========
        sku_specs = sku.specs.order_by('spec_id')
        sku_key = [spec.option.id for spec in sku_specs]

        skus = sku.spu.sku_set.all()

        spec_sku_map = {}
        for s in skus:
            s_specs = s.specs.order_by('spec_id')
            key = [spec.option.id for spec in s_specs]
            spec_sku_map[tuple(key)] = s.id

        goods_specs = sku.spu.specs.order_by('id')

        for index, spec in enumerate(goods_specs):
            key = sku_key[:]
            options = spec.options.all()

            for option in options:
                key[index] = option.id
                option.sku_id = spec_sku_map.get(tuple(key))

            spec.spec_options = list(options)

        context = {
            'categories': categories,
            'breadcrumb': breadcrumb,
            'sku': sku,
            'specs': goods_specs,
            'sku_images': sku_images,   # ← 关键
        }

        return render(request, 'detail.html', context)

添加templates/list.html

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>美多商城-商品列表</title>

    <link rel="stylesheet" href="{{ static('css/jquery.pagination.css') }}">
    <link rel="stylesheet" href="{{ static('css/reset.css') }}">
    <link rel="stylesheet" href="{{ static('css/main.css') }}">

    <script src="{{ static('js/jquery-1.12.4.min.js') }}"></script>
    <script src="{{ static('js/vue-2.5.16.js') }}"></script>
    <script src="{{ static('js/axios-0.18.0.min.js') }}"></script>

    <script>
        let category_id = "{{ category.id }}"
    </script>
</head>

<body>
<div id="app" v-cloak>

<!-- 顶部 -->
<div class="header_con">
    <div class="header">
        <div class="welcome fl">欢迎来到美多商城!</div>
        <div class="fr">
            <div class="login_btn fl" v-if="username">
                欢迎您:<em>[[ username ]]</em>
                <span>|</span>
                <a href="/logout/">退出</a>
            </div>
            <div class="login_btn fl" v-else>
                <a href="/login/">登录</a>
                <span>|</span>
                <a href="/register/">注册</a>
            </div>

            <div class="user_link fl">
                <span>|</span>
                <a href="/info/">用户中心</a>
                <span>|</span>
                <a href="/carts/">我的购物车</a>
                <span>|</span>
                <a href="/orders/">我的订单</a>
            </div>
        </div>
    </div>
</div>

<!-- 搜索 -->
<div class="search_bar clearfix">
    <a href="/" class="logo fl">
        <img src="{{ static('images/logo.png') }}">
    </a>

    <div class="search_wrap fl">
        <form method="get" action="/search/" class="search_con">
            <input type="text" class="input_text fl" name="q" placeholder="搜索商品">
            <input type="submit" class="input_btn fr" value="搜索">
        </form>
    </div>
</div>

<!-- 分类导航 -->
<div class="navbar_con">
    <div class="navbar">
        <div class="sub_menu_con fl">
            <h1 class="fl">商品分类</h1>
            <ul class="sub_menu">
                {% for group in categories.values() %}
                <li>
                    <div class="level1">
                        {% for channel in group.channels %}
                        <a href="{{ channel.url }}">{{ channel.name }}</a>
                        {% endfor %}
                    </div>
                    <div class="level2">
                        {% for cat2 in group.sub_cats %}
                        <div class="list_group">
                            <div class="group_name fl">{{ cat2.name }} &gt;</div>
                            <div class="group_detail fl">
                                {% for cat3 in cat2.sub_cats %}
                                <a href="/list/{{ cat3.id }}/1/">{{ cat3.name }}</a>
                                {% endfor %}
                            </div>
                        </div>
                        {% endfor %}
                    </div>
                </li>
                {% endfor %}
            </ul>
        </div>

        <ul class="navlist fl">
            <li><a href="/">首页</a></li>
            <li class="interval">|</li>
            <li><a href="#">真划算</a></li>
            <li class="interval">|</li>
            <li><a href="#">抽奖</a></li>
        </ul>
    </div>
</div>

<!-- 面包屑 -->
<div class="breadcrumb">
    <a href="javascript:;">{{ breadcrumb.cat1.name }}</a>
    <span>></span>
    <a href="javascript:;">{{ breadcrumb.cat2.name }}</a>
    <span>></span>
    <a href="javascript:;">{{ breadcrumb.cat3.name }}</a>
</div>

<!-- 商品列表 -->
<div class="main_wrap clearfix">
    <div class="r_wrap fr clearfix">

        <div class="sort_bar">
            <a href="/list/{{ category.id }}/{{ page_num }}/?sort=default"
               {% if sort == 'default' %}class="active"{% endif %}>默认</a>
            <a href="/list/{{ category.id }}/{{ page_num }}/?sort=price"
               {% if sort == 'price' %}class="active"{% endif %}>价格</a>
            <a href="/list/{{ category.id }}/{{ page_num }}/?sort=hot"
               {% if sort == 'hot' %}class="active"{% endif %}>人气</a>
        </div>

        <ul class="goods_type_list clearfix">
            {% for sku in page_skus %}
            <li>
                <a href="/detail/{{ sku.id }}/">
                    <img src="{{ sku.default_image.url }}">
                </a>
                <h4>{{ sku.name }}</h4>
                <div class="operate">
                    <span class="price">¥{{ sku.price }}</span>
                    <span class="unit">台</span>
                </div>
            </li>
            {% endfor %}
        </ul>

        <div class="pagenation">
            <div id="pagination"></div>
        </div>
    </div>
</div>

<script src="{{ static('js/jquery.pagination.min.js') }}"></script>
<script>
    $('#pagination').pagination({
        currentPage: {{ page_num }},
        totalPage: {{ total_page }},
        callback:function (current) {
            location.href = '/list/{{ category.id }}/' + current + '/?sort={{ sort }}';
        }
    })
</script>

</div>
</body>
</html>

添加templates/detail.html

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
<head>
	<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
	<title>美多商城-商品详情</title>
	<link rel="stylesheet" type="text/css" href="/static/css/reset.css">
	<link rel="stylesheet" type="text/css" href="/static/css/main.css">
    <script type="text/javascript" src="/static/js/host.js"></script>
	<script type="text/javascript" src="/static/js/common.js"></script>
	<script type="text/javascript" src="/static/js/vue-2.5.16.js"></script>
    <script type="text/javascript" src="/static/js/axios-0.18.0.min.js"></script>
</head>
<body>
<div id="app" v-cloak>

<!-- ================= 面包屑 ================= -->
<div class="breadcrumb">
	<a href="/">全部分类</a>
	<span>&gt;</span>
	<a href="#">{{ breadcrumb.cat1.name }}</a>
	<span>&gt;</span>
	<a href="#">{{ breadcrumb.cat2.name }}</a>
	<span>&gt;</span>
	<span>{{ breadcrumb.cat3.name }}</span>
</div>

<!-- ================= 商品详情 ================= -->
<div class="goods_detail_con clearfix">

	<!-- ===== 左侧商品图片(⚠️只用 sku_images) ===== -->
	<div class="goods_detail_pic fl">
		{% if sku_images %}
			<img src="{{ sku_images[0].image.url }}">
		{% else %}
			<img src="/static/images/no_image.png">
		{% endif %}
	</div>

	<!-- ===== 右侧商品信息 ===== -->
	<div class="goods_detail_list fr">
		<h3>{{ sku.name }}</h3>
		<p>{{ sku.caption }}</p>

		<div class="price_bar">
			<span class="show_pirce">
				¥<em>{{ sku.price }}</em>
			</span>
		</div>

		<div class="goods_num clearfix">
			<div class="num_name fl">数 量:</div>
			<div class="num_add fl">
				<input type="text" class="num_show fl" value="1">
				<a href="javascript:;" class="add fr">+</a>
				<a href="javascript:;" class="minus fr">-</a>
			</div>
		</div>

		<!-- ================= 规格选择(⚠️核心区) ================= -->
		{% for spec in specs %}
		<div class="type_select">
			<label>{{ spec.name }}:</label>

			{% for option in spec.spec_options %}
				{% if option.sku_id == sku.id %}
					<a href="javascript:;" class="select">{{ option.value }}</a>
				{% elif option.sku_id %}
					<a href="{{ url('goods:detail', args=(option.sku_id,)) }}">
						{{ option.value }}
					</a>
				{% else %}
					<a href="javascript:;">{{ option.value }}</a>
				{% endif %}
			{% endfor %}
		</div>
		{% endfor %}

		<div class="total">
			总价:<em>{{ sku.price }}元</em>
		</div>

		<div class="operate_btn">
			<a href="javascript:;" class="add_cart" id="add_cart">加入购物车</a>
		</div>
	</div>
</div>

<!-- ================= 商品详情 Tab ================= -->
<div class="main_wrap clearfix">
	<div class="r_wrap fr clearfix">
		<ul class="detail_tab clearfix">
			<li class="active">商品详情</li>
			<li>规格与包装</li>
			<li>商品评价</li>
			<li>售后服务</li>
		</ul>

		<div class="tab_content current">
			<dl>
				<dt>商品详情:</dt>
				<dd>{{ sku.spu.desc_detail }}</dd>
			</dl>
		</div>
	</div>
</div>

<div class="footer">
	<p>CopyRight © 2016 北京美多商业股份有限公司</p>
</div>

</div>

<script>
	var sku_id = "{{ sku.id }}";
</script>

<script type="text/javascript" src="/static/js/detail.js"></script>
</body>
</html>

迁移数据库

python manage.py makemigrations goods
python manage.py migrate
美多商城项目-05

数据结构详解

0. 先把“商品域”一句话讲透

  • SPU:一类商品(同一个“款”),比如「iPhone 15」
  • SKU:可买的具体商品(某个规格组合),比如「iPhone 15 / 128G / 黑色」
  • 规格:决定 SKU 的维度,比如「颜色」「容量」
  • 库存/价格:一定属于 SKU(因为不同规格价格库存不同)

所以:SPU 负责“商品内容/介绍”,SKU 负责“交易属性/可购买”


1. 类目表:GoodsCategory(tb_goods_category)

字段意义

  • name:类目名称(如 手机、智能手机、安卓手机)
  • parent:父类目(自关联),支持三级类目

为什么要三级类目?

电商常用三级结构:

  • 1级:手机数码
  • 2级:手机通讯
  • 3级:智能手机

典型数据

idnameparent_id
1手机数码null
2手机通讯1
3智能手机2

2. 频道表:GoodsChannelGroup / GoodsChannel

这两张表是给首页导航用的(类似京东顶部的分类栏目)。

GoodsChannelGroup(频道组)

  • name:组名(如 “手机/运营商/数码”)

GoodsChannel(频道)

  • group:属于哪个频道组
  • category:绑定一个顶级类目
  • url:点击跳转链接
  • sequence:排序

你可以理解成:“首页导航=频道组(列) + 类目(每项)”


3. 品牌表:Brand(tb_brand)

  • name:品牌名(Apple、小米)
  • logo:品牌 logo
  • first_letter:首字母(用于筛选/索引)

品牌和商品是 1 对多:

  • 一个品牌下面很多 SPU

4. 核心:SPU 表(tb_spu)——“款”的概念

SPU 是什么?

Standard Product Unit:标准产品单元
在电商里就是一个“商品款式”,它描述的不是“可下单的那一件”,而是“这个商品整体介绍”。

SPU 字段怎么理解

  • name:款名(如 “iPhone 15”)
  • brand:属于哪个品牌
  • category1/2/3:绑定三级类目(一般绑定到 3 级,但保留 1/2/3 做查询方便)
  • sales/comments:聚合数据(通常由 SKU 汇总得到)
  • desc_detail/desc_pack/desc_service
    • 详情介绍
    • 包装信息
    • 售后服务

为什么描述信息放 SPU,不放 SKU?

因为「iPhone 15」的商品介绍、售后说明,不因颜色容量变化,放在 SKU 会重复 N 次。


5. 核心:SKU 表(tb_sku)——“可购买单位”

SKU 是什么?

Stock Keeping Unit:库存单位
你真正下单买的,是 SKU。

SKU 字段怎么理解

  • name:SKU 名(“iPhone 15 128G 黑色”)
  • caption:副标题(营销文案)
  • spu:属于哪个 SPU(iPhone 15)
  • category:从属类别(通常就是 3 级类目)
  • price/cost_price/market_price
    • price:售卖价
    • cost_price:进货成本(用于利润/报表)
    • market_price:划线价
  • stock:库存(只属于 SKU)
  • sales/comments:SKU 自己的销量与评价(SPU 里也有一个汇总)
  • is_launched:是否上架(下架后不展示)
  • default_image:默认展示图

为什么库存一定在 SKU?

因为:

  • 黑色 128G 可能只剩 2 台
  • 蓝色 256G 可能还有 50 台
    库存不可能挂在 SPU 上。

6. 图片表:SKUImage(tb_sku_image)

  • 一个 SKU 有多张图片(轮播图)
  • sku:属于哪个 SKU
  • image:图片地址

为什么图片绑 SKU?
很多商品不同规格(颜色)图片不一样,所以绑 SKU 更准确。


7. 规格系统(最关键但最容易乱)

你这里有三张规格相关表:

A) SPUSpecification(tb_spu_specification)——“这个 SPU 有哪些规格维度”

例:iPhone 15 有两个规格维度:

  • 颜色
  • 容量

字段:

  • spu:属于哪个 SPU
  • name:规格名(颜色/容量)

SPU 级别定义“维度”,SKU 级别选择“具体值”。


B) SpecificationOption(tb_specification_option)——“某个规格有哪些选项值”

例:

  • 颜色:黑色、蓝色、粉色
  • 容量:128G、256G、512G

字段:

  • spec:属于哪个规格(颜色/容量)
  • value:选项值(黑色/128G)

C) SKUSpecification(tb_sku_specification)——“某个 SKU 在每个规格维度上选了哪个值”

例:
SKU = “iPhone 15 / 黑色 / 128G”
就会有两行:

  • spec=颜色,option=黑色
  • spec=容量,option=128G

字段:

  • sku:哪个 SKU
  • spec:规格维度(颜色/容量)
  • option:具体选项(黑色/128G)

8. 用一个完整例子把 SPU/SKU/规格串起来

例子:iPhone 15

1) SPU(iPhone 15)

  • name=“iPhone 15”
  • desc_detail=“xxx”
  • desc_service=“全国联保…”

2) SPU 规格维度(SPUSpecification)

  • 颜色
  • 容量

3) 规格选项(SpecificationOption)

  • 颜色:黑色、蓝色
  • 容量:128G、256G

4) SKU(可买的组合)

SKU1:iPhone 15 / 黑色 / 128G(price=5999 stock=10)
SKU2:iPhone 15 / 黑色 / 256G(price=6999 stock=5)
SKU3:iPhone 15 / 蓝色 / 128G(price=5999 stock=8)

5) SKU 规格明细(SKUSpecification)

SKU1:

  • 颜色=黑色
  • 容量=128G

SKU2:

  • 颜色=黑色
  • 容量=256G

9. 访问量表:GoodsVisitCount(tb_goods_visit)

  • 按“分类 + 日期”统计访问量
  • category:统计哪个分类
  • count:当日访问量
  • date:日期(自动写入)

用于运营后台看“哪些类目热”。

FastDFS

FastDFS 是一个开源的分布式文件存储系统,专门用来存“图片 / 文件”,不存业务数据。

美多商城项目-05

作用:

  • 它负责:文件上传 / 存储 / 访问
  • 它不负责:用户、订单、商品、库存这些业务逻辑
  • 它解决的问题是:“文件太多,不能直接放在 Web 服务器上”

FastDFS 的核心组成

FastDFS
├── Tracker(调度器)
│     └── 负责:告诉你文件该存到哪台机器
│
└── Storage(存储节点)
      └── 负责:真正存文件(图片、附件等)
美多商城项目-05

在「美多商城」中,FastDFS 存储图片

为什么美多商城要用 FastDFS

图片数量巨大,不能放在 Django 项目里

如果你把图片直接放在/media/

问题会是:

  • Django 服务器压力巨大
  • 多台 Web 服务器无法共享文件
  • 扩容基本没法做

数据库只存 URL,架构清晰

MySQL            FastDFS
------           --------
image_url  --->  /group1/M00/00/01/xxx.jpg

docker安装运行 FastDFS

项目根目录构建目录结构

docker-compose/
├── docker-compose.fastdfs-nginx.yml
├── tracker/
│   └── data/
├── storage/
│   ├── data/
│   └── logs/
└── nginx/
    ├── nginx.conf
    └── conf.d/
        └── fastdfs.conf

创建 docker-compose.fastdfs.yml

services:

  # =========================
  # FastDFS Tracker(调度节点)
  # =========================
  tracker:
    image: delron/fastdfs                  # 使用 FastDFS 官方镜像
    container_name: fastdfs-tracker        # Tracker 容器名称
    command: tracker                       # 指定启动角色为 Tracker
    ports:
      - "22122:22122"                      # Tracker 服务端口
                                            # 左边:宿主机端口
                                            # 右边:容器内端口
    volumes:
      - ./tracker:/var/fdfs               # Tracker 的运行数据和日志
                                            # 左边:宿主机目录
                                            # 右边:容器内 base_path


  # =========================
  # FastDFS Storage(存储节点)
  # =========================
  storage:
    image: delron/fastdfs                  # 同一个镜像,不同角色
    container_name: fastdfs-storage        # Storage 容器名称
    command: storage                       # 指定启动角色为 Storage
    environment:
      TRACKER_SERVER: fastdfs-tracker:22122
                                            # Storage 启动时注册到哪个 Tracker
                                            # fastdfs-tracker 是 Docker 内部 DNS 名
    ports:
      - "23000:23000"                      # Storage 服务端口
                                            # 主要用于调试 / 内部通信
    volumes:
      - ./storage:/var/fdfs               # Storage 文件真实落盘位置
                                            # ./storage/data 里存的就是 FastDFS 文件
    depends_on:
      - tracker                            # 启动顺序:先 Tracker,后 Storage


  # =========================
  # Nginx(FastDFS 的 HTTP 访问入口)
  # =========================
  nginx:
    image: nginx:1.25-alpine               # 官方 Nginx 镜像(alpine 体积小)
    container_name: fastdfs-nginx          # Nginx 容器名称
    ports:
      - "8888:80"                          # HTTP 访问端口
                                            # 浏览器 / Django 访问:http://127.0.0.1:8888
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
                                            # Nginx 主配置文件
                                            # ro = 只读,防止容器内误改

      - ./nginx/conf.d:/etc/nginx/conf.d:ro
                                            # Nginx 站点配置目录
                                            # 这里放 fastdfs.conf

      - ./storage/data:/var/fdfs/data:ro
                                            # 把 FastDFS Storage 的数据目录
                                            # 挂载给 Nginx 用于直接读取文件
                                            # ro = 只读,Nginx 不写文件
    depends_on:
      - storage                            # 启动顺序:先 Storage,后 Nginx

Nginx 主配置

nginx/nginx.conf

user  nginx;
worker_processes auto;

events {
    worker_connections 1024;
}

http {
    include       mime.types;
    default_type  application/octet-stream;

    sendfile        on;
    keepalive_timeout  65;

    include /etc/nginx/conf.d/*.conf;
}

FastDFS 文件访问配置

nginx/conf.d/fastdfs.conf

server {
    listen 80;
    server_name localhost;

    location /group1/M00 {
        alias /var/fdfs/data;
        autoindex on;
    }
}

架构:

美多商城项目-05

FastDFS上传图片文件

目前架构初步认定Django 在宿主机跑,调用过程为:

            ┌────────────────────┐
            │     Django 应用     │
            │ (Python / ORM / API)│
            └─────────┬──────────┘
                      │
          只保存 / 使用 file_id
                      │
                      ▼
            ┌────────────────────┐
            │        数据库       │
            │  group1/M00/...    │
            └────────────────────┘
                      │
              通过 HTTP 访问
                      │
                      ▼
            ┌────────────────────┐
            │        Nginx       │
            │  http://:8888      │
            │  alias /var/fdfs   │
            └─────────┬──────────┘
                      │ 读文件
                      ▼
            ┌────────────────────┐
            │   FastDFS Storage  │
            │   ./storage/data   │
            └─────────┬──────────┘
                      │ 注册 / 心跳
                      ▼
            ┌────────────────────┐
            │   FastDFS Tracker  │
            └────────────────────┘

安装客户端

python -m pip install fdfs_client-py

utils 目录下构建包,目录结构为

美多商城项目-05

utils/fdfs/client.conf

# ===== FastDFS client config =====

# 连接超时(秒)
connect_timeout=30

# 网络超时(秒)
network_timeout=120

# 客户端日志目录(必须存在)
base_path=/tmp

# Tracker 地址(通过 docker-compose 暴露出来的端口)
tracker_server=127.0.0.1:22122

# 日志级别
log_level=info

# 是否使用连接池
use_connection_pool = false

# 是否从 tracker 加载参数
load_fdfs_parameters_from_tracker=false

utils/fdfs/faststorage.py

from django.core.files.storage import Storage
from django.utils.deconstruct import deconstructible
from django.conf import settings


@deconstructible
class MyStorage(Storage):
    """
    FastDFS(HTTP/Nginx 模式)自定义存储后端
    Django 不直连 FastDFS
    """

    def _open(self, name, mode='rb'):
        # FastDFS 不支持 Django open
        raise NotImplementedError("FastDFS does not support open")

    def _save(self, name, content, max_length=None):
        """
        HTTP 模式下:
        - Django 不负责真正上传
        - 前端 / 其他服务已经上传完成
        - name 就是 FastDFS 返回的 file_id
        """
        return name

    def exists(self, name):
        # FastDFS 不做重名判断
        return False

    def url(self, name):
        """
        name = file_id
        """
        return settings.FDFS_BASE_URL + name

meiduo_mall/settings.py 添加FastDFS自定义文件存储配置:

# ================= FastDFS配置 =================

# FastDFS 通过 Nginx 访问的前缀
FDFS_BASE_URL = "http://127.0.0.1:8888/"

# 指定默认存储后端
DEFAULT_FILE_STORAGE = "utils.fdfs.faststorage.MyStorage"

# 媒体
MEDIA_URL = 'http://127.0.0.1:8888/'

运行FastDFS

docker compose -f docker-compose.fastdfs.yml up -d
美多商城项目-05
  • FastDFS 已经是“文件仓库”,
  • Nginx 是“HTTP 文件服务器”,
  • Django :不再参与上传、不再调用 FastDFS 客户端、不再关心 Tracker。

测试FastDFS 里文件可以访问:

进入 Storage 容器

docker exec -it fastdfs-storage bash

用 FastDFS 命令上传一个测试文件(推荐)

fdfs_upload_file /etc/fdfs/client.conf /etc/hosts

你会看到输出类似:

group1/M00/00/00/rBMAA2legAuAFWN3AAAArICyYWY0983146
美多商城项目-05

这个字符串 就是 file_id

确认文件在宿主机真实落盘

find docker-compose/storage/data -type f | head
美多商城项目-05

用浏览器HTTP 测试

http://127.0.0.1:8888/group1/M00/00/00/rBMAA2legAuAFWN3AAAArICyYWY0983146

host成功下载:

美多商城项目-05

录入商品数据和图片数据

mysql -u root -p meiduo_mall < goods_data_8.0.sql

SQL 主要导入了哪些数据?

① 商品分类数据(导航菜单用)

涉及表

  • tb_channel_group
  • tb_goods_channel
  • tb_goods_category
美多商城项目-05

② 品牌 + 商品基础数据

涉及表

  • tb_brand
  • tb_spu
  • tb_sku
  • tb_sku_image

导入了什么?

  • 品牌:华为 / 小米 / 360
  • SPU:360手机 N6 Pro
  • SKU:
    • 黑色 64G ¥16.80
    • 白色 128G ¥28.80
  • 商品图片

③ 商品规格数据(颜色 / 内存 / 尺寸)

涉及表

  • tb_spu_specification
  • tb_specification_option
  • tb_sku_specification

导入了什么?

  • 规格名:颜色 / 内存
  • 规格值:黑色 / 白色 / 64G / 128G
  • SKU 与规格的对应关系

④ 首页内容管理数据(轮播图 / 楼层)等CMS 数据

涉及表

  • tb_content_category
  • tb_content

导入了什么?

  • 首页轮播图
  • 首页快讯
  • 1F / 2F / 3F 楼层商品
  • 推荐位、广告位
美多商城项目-05

解压并导入 fdfs 图片

链接: https://pan.baidu.com/s/1Im4Wb7K_Yrj03lZEfwQy2g 提取码: qw37

docker compose -f docker-compose.fastdfs.yml down
tar -xzvf data.tar.gz -C storage/
docker compose -f docker-compose.fastdfs.yml up -d

测试访问图片:

可以先找个图片

file storage/data/00/00/*  | grep -i image | head
美多商城项目-05

就拿这个图片文件为例:

file storage/data/00/00/CtM3BVni03-ANUDwAAAmv27pX4k9203075

修改 meiduo_mall/docker-compose/nginx/nginx.conf

user  nginx;
worker_processes auto

events {
    worker_connections 1024;
}

http {
    include       mime.types;
    default_type  application/octet-stream;

    sendfile        on;
    keepalive_timeout  65;

    include /etc/nginx/conf.d/*.conf;
}

修改 meiduo_mall/docker-compose/nginx/conf.d/fastdfs.conf

server {
    listen 80;
    server_name localhost;

    location /group1/M00/ {
        alias /var/fdfs/data/;

        # FastDFS 图片无扩展名,统一当图片处理
        default_type image/jpeg;
        add_header Content-Disposition inline;

        # 强缓存(商品图必开)
        expires 30d;
        add_header Cache-Control "public";

        # 禁止目录列表
        autoindex off;

        # 防盗链(按需)
        valid_referers none blocked img.yourdomain.com;
        if ($invalid_referer) {
            return 403;
        }
    }
}

docker restart fastdfs-nginx

测试 nginx 访问路径:http://127.0.0.1:8888/group1/M00/00/00/CtM3BVni03-ANUDwAAAmv27pX4k9203075

美多商城项目-05

安装解析音视频“内容本身”的信息,主动向“外部 HTTP 服务”发请求

pip install mutagen
pip install requests

商品首页展示

封装首页商品频道分类以及展示首页商品广告

新增商品内容模块

cd apps 
python ../manage.py startapp contents

meiduo_mall/urls.py

urlpatterns = [
    # 商品模块
    path("", include(("apps.goods.urls", "goods"), namespace="goods")),
    path("", include(("apps.contents.urls", "contents"), namespace="contents")),
]

apps 添加 contents

meiduo_mall/settings.py
INSTALLED_APPS = [
    ...
    'apps.contents',
]

apps/contents/apps.py

from django.apps import AppConfig


class ContentsConfig(AppConfig):
    default_auto_field = "django.db.models.BigAutoField"
    name = "apps.contents"

apps/contents/models.py

from django.db import models
from utils.models import BaseModel
from django.conf import settings
from utils.fdfs.faststorage import MyStorage

class ContentCategory(BaseModel):
    """广告内容类别"""
    name = models.CharField(max_length=50, verbose_name='名称')
    key = models.CharField(max_length=50, verbose_name='类别键名')

    class Meta:
        db_table = 'tb_content_category'
        verbose_name = '广告内容类别'
        verbose_name_plural = verbose_name

    def __str__(self):
        return self.name


class Content(BaseModel):
    """广告内容"""
    category = models.ForeignKey(
        ContentCategory,
        on_delete=models.PROTECT,
        verbose_name='类别'
    )
    title = models.CharField(max_length=100, verbose_name='标题')
    url = models.CharField(max_length=300, verbose_name='内容链接')
    image = models.ImageField(storage=MyStorage(),null=True, blank=True, verbose_name='图片')
    text = models.TextField(null=True, blank=True, verbose_name='内容')
    sequence = models.IntegerField(verbose_name='排序')
    status = models.BooleanField(default=True, verbose_name='是否展示')

    class Meta:
        db_table = 'tb_content'
        verbose_name = '广告内容'
        verbose_name_plural = verbose_name

    def __str__(self):
        return self.category.name + ': ' + self.title

apps/contents/urls.py

from django.urls import path
from .views import IndexView

app_name = "contents"

urlpatterns = [
    path("", IndexView.as_view(), name="index"),
]

新增:apps/contents/utils.py

from collections import OrderedDict
from apps.goods.models import GoodsChannel

def get_categories():
    """
    封装首页商品频道和分类数据
    返回结构:
    {
        group_id: {
            'channels': [...],
            'sub_cats': [...]
        }
    }
    """
    categories = OrderedDict()

    # 按频道组 + 组内顺序排序
    channels = GoodsChannel.objects.select_related(
        'group', 'category'
    ).order_by('group_id', 'sequence')

    for channel in channels:
        group_id = channel.group_id

        if group_id not in categories:
            categories[group_id] = {
                'channels': [],
                'sub_cats': []
            }

        # 一级分类
        cat1 = channel.category

        categories[group_id]['channels'].append({
            'id': cat1.id,
            'name': cat1.name,
            'url': channel.url
        })

        # 二级 / 三级分类
        for cat2 in cat1.subs.all():
            cat2.sub_cats = []
            for cat3 in cat2.subs.all():
                cat2.sub_cats.append(cat3)
            categories[group_id]['sub_cats'].append(cat2)

    return categories

apps/contents/views.py

# apps/contents/views.py
from django.shortcuts import render
from django.views import View

from apps.contents.utils import get_categories
from apps.contents.models import ContentCategory
from django.conf import settings

class IndexView(View):
    def get(self, request):
        # 1. 商品分类(左侧分类栏)
        categories = get_categories()

        # 2. 首页内容(轮播 / 快讯 / 楼层)
        contents = {}
        content_categories = ContentCategory.objects.all()
        for cat in content_categories:
            contents[cat.key] = cat.content_set.filter(
                status=True
            ).order_by('sequence')

        context = {
            'categories': categories,
            'contents': contents,
            'FDFS_BASE_URL': settings.FDFS_BASE_URL,
        }
        return render(request, 'index.html', context)

完善首页:templates/index.html

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
<head>
	<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
	<title>美多商城-首页</title>
	<link rel="stylesheet" type="text/css" href="../static/css/reset.css">
	<link rel="stylesheet" type="text/css" href="../static/css/main.css">
    <script type="text/javascript" src="../static/js/host.js"></script>
	<script type="text/javascript" src="../static/js/common.js"></script>
	<script type="text/javascript" src="../static/js/vue-2.5.16.js"></script>
    <script type="text/javascript" src="../static/js/axios-0.18.0.min.js"></script>
	<script type="text/javascript" src="../static/js/jquery-1.12.4.min.js"></script>
    <script>
    	var username = "{{ request.user.username | default('') }}";
    </script>
</head>

<body>
	<div id="app" v-cloak>
	<div class="header_con">
		<div class="header">
			<div class="welcome fl">欢迎来到美多商城!</div>
			<div class="fr">
				<div class="login_btn fl" v-if="username">
					欢迎您:<em>[[ username ]]</em>
					<span>|</span>
					<a href="{{ url('users:logout') }}" >退出</a>
				</div>

				<div class="login_btn fl" v-else>
					<a href="{{ url('users:login') }}">登录</a>
					<span>|</span>
					<a href="{{ url('users:register') }}">注册</a>
				</div>

				<div class="user_link fl">
					<span>|</span>
					<a href="users/user_center_info.html">用户中心</a>
					<span>|</span>
					<a href="../static/cart.html">我的购物车</a>
					<span>|</span>
					<a href="../static/user_center_order.html">我的订单</a>
				</div>
			</div>
		</div>		
	</div>

	<div class="search_bar clearfix">
		<a href="index.html" class="logo fl"><img src="../static/images/logo.png"></a>
		<div class="search_wrap fl">
			<form method="get" action="/search/" class="search_con">
                <input type="text" class="input_text fl" name="q" placeholder="搜索商品">
                <input type="submit" class="input_btn fr" name="" value="搜索">
            </form>
			<ul class="search_suggest fl">
				<li><a href="#">索尼微单</a></li>
				<li><a href="#">优惠15元</a></li>
				<li><a href="#">美妆个护</a></li>
				<li><a href="#">买2免1</a></li>
			</ul>
		</div>	
		
		<div class="guest_cart fr">
            <a href="/carts/" class="cart_name fl">我的购物车</a>
            <div class="goods_count fl" id="show_count">[[ cart_total_count ]]</div>
            <ul class="cart_goods_show">
                <li v-for="cart in carts">
                    <img :src="cart.default_image_url" alt="商品图片">
                    <h4>[[ cart.name ]]</h4>
                    <div>[[ cart.count ]]</div>
                </li>
            </ul>
		</div>
	</div>

	<div class="navbar_con">
		<div class="navbar">
			<h1 class="fl">商品分类</h1>
			<ul class="navlist fl">
				<li><a href="">首页</a></li>
				<li class="interval">|</li>
				<li><a href="">真划算</a></li>
				<li class="interval">|</li>
				<li><a href="">抽奖</a></li>
			</ul>
		</div>
	</div>

	<!-- ================= 轮播图 ================= -->
	<div class="pos_center_con clearfix">
		<ul class="slide">
			{% for content in contents.index_lbt %}
			<li>
				<a href="{{ content.url }}">
					<img src="{{ content.image.url }}" alt="{{ content.title }}">
				</a>
			</li>
			{% endfor %}
		</ul>

		<ul class="sub_menu">
			{% for group in categories.values() %}
			<li>
				<div class="level1">
					{% for channel in group.channels %}
					<a href="{{ channel.url }}">{{ channel.name }}</a>
					{% endfor %}
				</div>

				<div class="level2">
					{% for cat2 in group.sub_cats %}
					<div class="list_group">
						<div class="group_name fl">{{ cat2.name }} &gt;</div>
						<div class="group_detail fl">
							{% for cat3 in cat2.sub_cats %}
							<a href="/list/{{ cat3.id }}/1/">{{ cat3.name }}</a>
							{% endfor %}
						</div>
					</div>
					{% endfor %}
				</div>
			</li>
			{% endfor %}
		</ul>

		<!-- 快讯 -->
		<div class="news">
			<div class="news_title">
				<h3>快讯</h3>
			</div>
			<ul class="news_list">
				{% for content in contents.index_kx %}
				<li><a href="{{ content.url }}">{{ content.title }}</a></li>
				{% endfor %}
			</ul>

			{% for content in contents.index_ytgg %}
			<a href="{{ content.url }}" class="advs">
				<img src="{{ FDFS_BASE_URL }}{{ content.image }}">
			</a>
			{% endfor %}
		</div>
	</div>


	<!-- ================= 1F ================= -->
	<div class="list_model">
		<div class="list_title clearfix">
			<h3 class="fl">1F</h3>
		</div>

		<div class="goods_con clearfix">
			<div class="goods_banner fl">
				<img src="{{ contents.index_1f_logo.0.image.url }}">

				<div class="channel">
					{% for content in contents.index_1f_pd %}
					<a href="{{ content.url }}">{{ content.title }}</a>
					{% endfor %}
				</div>

				<div class="key_words">
					{% for content in contents.index_1f_bq %}
					<a href="{{ content.url }}">{{ content.title }}</a>
					{% endfor %}
				</div>
			</div>

			<div class="goods_list_con">
				<ul class="goods_list fl">
					{% for content in contents.index_1f_ssxp %}
					<li>
						<a href="{{ content.url }}" class="goods_pic">
							<img src="{{ content.image.url }}">
						</a>
						<h4><a href="{{ content.url }}">{{ content.title }}</a></h4>
						<div class="price">{{ content.text }}</div>
					</li>
					{% endfor %}
				</ul>
			</div>
		</div>
	</div>

	<!-- ================= 2F ================= -->
	<div class="list_model model02">
		<div class="list_title clearfix">
			<h3 class="fl">2F</h3>
		</div>

		<div class="goods_con clearfix">
			<div class="goods_banner fl">
				<img src="{{ contents.index_2f_logo.0.image.url }}">

				<div class="channel">
					{% for content in contents.index_2f_pd %}
					<a href="{{ content.url }}">{{ content.title }}</a>
					{% endfor %}
				</div>

				<div class="key_words">
					{% for content in contents.index_2f_bq %}
					<a href="{{ content.url }}">{{ content.title }}</a>
					{% endfor %}
				</div>
			</div>

			<div class="goods_list_con">
				<ul class="goods_list fl">
					{% for content in contents.index_2f_cxdj %}
					<li>
						<a href="{{ content.url }}" class="goods_pic">
							<img src="{{ content.image.url }}">
						</a>
						<h4><a href="{{ content.url }}">{{ content.title }}</a></h4>
						<div class="price">{{ content.text }}</div>
					</li>
					{% endfor %}
				</ul>
			</div>
		</div>
	</div>

	<!-- ================= 3F ================= -->
	<div class="list_model model03">
		<div class="list_title clearfix">
			<h3 class="fl">3F</h3>
		</div>

		<div class="goods_con clearfix">
			<div class="goods_banner fl">
				<img src="{{ contents.index_3f_logo.0.image.url }}">

				<div class="channel">
					{% for content in contents.index_3f_pd %}
					<a href="{{ content.url }}">{{ content.title }}</a>
					{% endfor %}
				</div>

				<div class="key_words">
					{% for content in contents.index_3f_bq %}
					<a href="{{ content.url }}">{{ content.title }}</a>
					{% endfor %}
				</div>
			</div>

			<div class="goods_list_con">
				<ul class="goods_list fl">
					{% for content in contents.index_3f_shyp %}
					<li>
						<a href="{{ content.url }}" class="goods_pic">
							<img src="{{ content.image.url }}">
						</a>
						<h4><a href="{{ content.url }}">{{ content.title }}</a></h4>
						<div class="price">{{ content.text }}</div>
					</li>
					{% endfor %}
				</ul>
			</div>
		</div>
	</div>

	<div class="footer">
		<div class="foot_link">
			<a href="#">关于我们</a>
			<span>|</span>
			<a href="#">联系我们</a>
			<span>|</span>
			<a href="#">招聘人才</a>
			<span>|</span>
			<a href="#">友情链接</a>		
		</div>
		<p>CopyRight © 2016 北京美多商业股份有限公司 All Rights Reserved</p>
		<p>电话:010-****888    京ICP备*******8号</p>
	</div>
	</div>
	<script type="text/javascript" src="../static/js/slide.js"></script>
	<script type="text/javascript" src="../static/js/common.js"></script>
	<script type="text/javascript" src="../static/js/index.js"></script>
</body>
</html>

访问首页:http://127.0.0.1:8000

可以用 navicat 修改数据测试数据来自数据库

美多商城项目-05
美多商城项目-05
美多商城项目-05

导航栏支持点击进入,实现商品的初步查看

美多商城项目-05
美多商城项目-05

发布者:LJH,转发请注明出处:https://www.ljh.cool/44536.html

(0)
LJH的头像LJH
上一篇 2025年12月28日 上午1:01
下一篇 2025年11月18日 下午8:36

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注