资讯动态

Django电商网站实战:从模型设计到订单库存一致性的关键路径

发布时间:2026/10/14 17:41:49 来源:尧图企业网站定制
简介这套资源是一份基于Django框架的电子商务网站完整项目面向Python Web开发学习者、Django初学者及需要快速搭建电商平台的技术人员用来理解MVT开发模式、模板渲染、ORM建模、用户认证与购物车逻辑。压缩包共101个文件涵盖17个Python源码、15个HTML页面、36张JPG图片以及SQLite数据库文件等整体仅3.89MB轻量但结构完整。目前已有91人学习下载。项目实现了商品展示、分类搜索、购物车管理、订单结账、注册登录等核心功能并利用Django模板语言动态渲染页面后台管理配合ORM模型可直观看到Product、Category、Order等数据表的字段关系。HTML模板覆盖首页、商品详情、搜索、结账等场景适合对照源码逐层拆解前后端交互也能直接用于课程设计或毕业设计的基础参考。通过阅读源码可掌握URL路由配置、视图函数编写、数据模型设计及静态资源组织方式同时体会Django在CSRF/XSS防护、缓存机制等安全与性能层面的实际用法。1. 一个Django电商网站到底要解决什么问题你打开一个电商网站第一眼看到的往往是商品列表和搜索框但真正让开发者睡不着觉的是购物车、下单、库存扣减、订单状态这一连串流程。用Django做电商网站最大的价值不是让你写出一个能展示商品的页面而是让你在一个框架里把用户、商品、购物车、订单、支付回调这条业务闭环顺利串起来。EcommerceWbsite 这个项目就是用 Django 从零搭一个这类电商网站它覆盖了商品录入、前台浏览、购物车、下单、库存管理、订单状态流转这些常规功能也暴露了新手最容易踩的几个坑。这个项目适合两类人一类是刚学完Django基础、想找一个综合项目验证自己能力的人另一类是团队里要快速交付一个电商MVP、但又不想一上来就上微服务或者无头架构的人。它解决的核心问题很具体让订单数据和库存数据在同一个事务里保持一致让后台改价格而不是改代码让购物车在匿名和登录状态下都能工作。同时它也明确告诉你不该做什么——别指望纯Django单机扛住秒杀流量别把订单金额存成浮点数别在用户还没付款前就把库存扣死。读懂这些边界你才算真正看懂这个项目。2. 选型与模型设计为什么Django适合做电商网站以及商品、订单、用户的核心模型2.1 用Django做电商的底气和边界电商网站的本质是交易系统的持久化与状态流转这部分Django的ORM、Admin后台、Session机制和事务支持正好能覆盖。ORM让你用Python定义关系型数据表不用手写大量SQLAdmin后台可以直接当商品录入界面用省去早期开发做管理端的成本Django自带的事务和锁机制让下单扣库存这种必须原子操作的逻辑有地方落地。对于日订单量在几千到几万的小型电商或者行业垂直商城这套组合足够可靠。但边界也很清楚。Django默认是同步阻塞模型高并发秒杀场景下一个请求卡在数据库锁上整个服务就会连锁堆积。常见做法是把Django当作业务核心用Redis扛热点流量用消息队列做异步订单处理而不是让Django裸奔扛全量并发。另一个边界是模型设计。电商的模型看起来简单但如果在字段类型上贪方便——比如用FloatField存钱、用DateTimeField存库存快照——后面对账和统计的时候会非常痛苦。我在建模前通常会先把这几个边界想清楚钱必须用DecimalField库存必须是非负整数订单必须存下单时的快照而不是引用实时的商品价格。2.2 用户、商品、购物车、订单四张关键表的字段设计先看用户表。不要单独建一张UserProfile表而是通过继承AbstractUser扩展Django自带的User。常见的做法是增加手机号、昵称、头像、积分余额这几个字段。继承的好处是能直接使用Django的认证体系和request.user不用在视图里手动拼接用户信息。商品表是电商建模的核心。简单项目里可以不做SPU/SKU的复杂分层但至少要把商品和分类分开。Product表的核心字段包括标题、图片、描述、分类外键、价格、库存、上架状态、创建时间。有几个字段容易漏销量累计、商品编码、上下架时间。商品编码很重要后面做订单对账和库存盘点都靠它上下架时间比单纯一个is_active布尔值更有用因为你可以用它做定时上下架。购物车表的设计有个常见误区把整个商品信息复制一份到购物车。购物车应该只保存用户ID、商品ID、数量、勾选状态、加入时间价格以实时商品价格为主。这样用户改价后购物车里显示的是最新价格最后下单时再以商品表价格为准生成订单快照。订单表要拆成主表和子表。Order主表存订单号、用户、订单总金额、收货信息快照、订单状态、创建时间OrderItem子表存商品名称快照、单价快照、商品SKU外键、数量、小计。这里强调快照两个字是因为下单后商品改价、改名、甚至下架都不应该影响已生成的订单。字段设计如果漏了快照后面售后和财务对账必然翻车。表核心字段关键约束Userphone, nickname, avatar, points继承AbstractUser避免单独关联表Categoryname, parentparent可空做多级分类Producttitle, price, stock, code, is_activeprice用DecimalFieldstock要带索引CartItemuser, product, quantity, selected一对多product外键加索引Orderorder_no, user, total_amount, status, address_snapshot订单号唯一金额带DecimalOrderItemorder, product, product_snapshot, price_snapshot, quantity保存快照不直接关联实时价格2.3 一个能落地的models.py骨架下面这段模型代码是EcommerceWbsite项目里最核心的部分覆盖了从用户到订单的完整链路。注意里面出现的注释每一处都是踩过坑后留下的。from django.db import models from django.contrib.auth.models import AbstractUser class User(AbstractUser): phone models.CharField(max_length20, blankTrue, nullTrue, verbose_name手机号) points models.IntegerField(default0, verbose_name积分) class Meta: verbose_name 用户 verbose_name_plural verbose_name class Category(models.Model): name models.CharField(max_length100, uniqueTrue, verbose_name分类名) parent models.ForeignKey( self, on_deletemodels.CASCADE, nullTrue, blankTrue, related_namechildren, verbose_name父分类, ) def __str__(self): return self.name class Product(models.Model): code models.CharField(max_length32, uniqueTrue, verbose_name商品编码) title models.CharField(max_length200, verbose_name商品标题) category models.ForeignKey( Category, on_deletemodels.PROTECT, related_nameproducts, verbose_name分类, ) price models.DecimalField(max_digits10, decimal_places2, verbose_name单价) stock models.PositiveIntegerField(default0, verbose_name库存) is_active models.BooleanField(defaultTrue, verbose_name是否上架) sales_count models.PositiveIntegerField(default0, verbose_name销量) created_at models.DateTimeField(auto_now_addTrue, verbose_name创建时间) class Meta: ordering [-created_at] def __str__(self): return f{self.code}-{self.title} class CartItem(models.Model): user models.ForeignKey( User, on_deletemodels.CASCADE, related_namecart_items, verbose_name用户, ) product models.ForeignKey( Product, on_deletemodels.CASCADE, related_namecart_items, verbose_name商品, ) quantity models.PositiveIntegerField(default1, verbose_name数量) selected models.BooleanField(defaultTrue, verbose_name勾选状态) class Meta: unique_together (user, product) verbose_name 购物车项 verbose_name_plural verbose_name class Order(models.Model): STATUS_CHOICES [ (pending, 待支付), (paid, 已支付), (shipped, 已发货), (completed, 已完成), (canceled, 已取消), ] order_no models.CharField(max_length40, uniqueTrue, verbose_name订单号) user models.ForeignKey( User, on_deletemodels.PROTECT, related_nameorders, verbose_name用户, ) total_amount models.DecimalField(max_digits10, decimal_places2, verbose_name订单金额) status models.CharField(max_length20, choicesSTATUS_CHOICES, defaultpending, verbose_name订单状态) address_snapshot models.TextField(verbose_name收货信息快照) created_at models.DateTimeField(auto_now_addTrue, verbose_name创建时间) class Meta: ordering [-created_at] def __str__(self): return self.order_no class OrderItem(models.Model): order models.ForeignKey( Order, on_deletemodels.CASCADE, related_nameitems, verbose_name订单, ) product models.ForeignKey( Product, on_deletemodels.PROTECT, nullTrue, related_nameorder_items, verbose_name商品外键, ) product_snapshot models.TextField(verbose_name商品信息快照) price_snapshot models.DecimalField(max_digits10, decimal_places2, verbose_name单价快照) quantity models.PositiveIntegerField(default1, verbose_name数量) subtotal models.DecimalField(max_digits10, decimal_places2, verbose_name小计)逻辑上要说明几点。Product的category外键用的是PROTECT而不是CASCADE目的是防止有商品挂在分类下时误删分类stock用PositiveIntegerField从数据库层面保证库存不为负数。CartItem设置了unique_together同一用户同一商品只能有一条购物车记录这样每次加购只需要更新quantity不会出现同一个商品在购物车里出现多行。OrderItem里保存了product外键但查询和展示时优先用product_snapshot和price_snapshot这样即使商品被删除或改名历史订单依然完整。参数设计上price和total_amount统一用DecimalFieldmax_digits10代表最多存10位数字decimal_places2代表保留两位小数。这里不要为了省空间用max_digits6因为订单金额可能需要累计万一超过上限会直接报错。order_no是uniqueTrue后面第6章会单独讲它的生成策略。2.4 建模时三个容易忽略的细节第一个是索引。很多新手用ORM之后忘记加db_index结果数据库一有几千条商品后台列表页就明显变慢。常用做法是对Product的外键category、CartItem的user字段、OrderItem的order字段都加上索引或者至少用ForeignKey默认创建索引。Django的ForeignKey默认会建索引但ManyToMany或字符串字段要用正文字段。第二是删除策略。商品被下单后不该被物理删除正确做法是is_activeFalse下架。OrderItem里的product外键用on_deletePROTECT这样如果商品被人尝试删除数据库会拦截。你可能会疑惑为什么不用SET_NULL因为SKU一旦被订单引用置空会让订单明细失去关联。我通常保留外键同时用快照字段兜底。第三是不要为了省表把订单主表和子表合并成一张表。若要支持一个订单包含多个商品就必须拆表否则改商品数量会非常尴尬。拆表会带来多一次查询但Django的related_name能让你用order.items.all()一次拿到明细成本可控。3. 从零到能跑项目初始化命令与配置清单3.1 从空目录到项目起来常用命令序列EcommerceWbsite 这种项目我习惯从空目录开始搭建而不是直接复制别人的模板。原因是电商项目的配置项很多一步步来才知道每个配置的作用。下面是一套最常用的初始化命令。mkdir ecommerce_website cd ecommerce_website python -m venv venv source venv/bin/activate # Windows下用 venv\Scripts\activate pip install django django-admin startproject config . python manage.py startapp shop python manage.py startapp order执行完这些命令后项目根目录会有config配置目录和shop、order两个应用目录。startproject后面的点表示在当前目录生成配置文件这样可以避免嵌套一层目录。startapp shop负责商品展示和购物车startapp order负责订单和结算业务边界拆清楚后面写迁移文件不容易互相干扰。有的开发者喜欢把应用叫别的名字比如catalog、sales都可以只要在INSTALLED_APPS里写对就行。我建议把项目配置目录叫config而不是内层再套同名目录因为后续部署时gunicorn或uwsgi的配置指向更清楚。建议在项目根目录初始化Git仓库每个阶段提交一次这样改模型字段后方便回滚。3.2 settings.py中电商网站必调的几处配置Django的settings.py默认配置能跑但电商网站必须在几个地方显式调整否则上线后就会出幺蛾子。下面是一个精简后的配置块直接适配EcommerceWbsite。# settings.py INSTALLED_APPS [ django.contrib.admin, django.contrib.auth, django.contrib.contenttypes, django.contrib.sessions, django.contrib.messages, django.contrib.staticfiles, shop, order, ] AUTH_USER_MODEL shop.User # 必须在你第一次迁移之前设置 DATABASES { default: { ENGINE: django.db.backends.mysql, NAME: ecommerce, USER: root, PASSWORD: your_password, HOST: 127.0.0.1, PORT: 3306, } } LANGUAGE_CODE zh-hans TIME_ZONE Asia/Shanghai USE_TZ True MEDIA_URL /media/ MEDIA_ROOT BASE_DIR / media STATIC_URL /static/ STATIC_ROOT BASE_DIR / staticfiles这里最关键的配置是AUTH_USER_MODEL。如果你继承了AbstractUser就一定要在第一次执行makemigrations前设置它否则Django已经用默认User表生成了迁移后面想改就非常麻烦那才是真正的黑匣子。数据库用MySQL时NAME、USER、PASSWORD按实际填写但要注意MySQL必须用utf8mb4编码否则中文特别是emoji商品名写入时可能报错。MEDIA_ROOT是用户上传的图片文件路径STATIC_ROOT是collectstatic后集中存放静态文件的路径。本地调试时还要在项目根urls.py里补充静态和媒体的serve规则否则商品图片加载不出来。3.3 注册模型到后台并跑通第一个商品页模型定义好了配置也改了接下来要通过后台把商品录进去再让前台能看见。python manage.py makemigrations python manage.py migrate python manage.py createsuperuser python manage.py runservermakemigrations会扫描所有应用模型生成迁移文件migrate把迁移执行到数据库。createsuperuser会让你输入用户名和邮箱密码这个账号用来登录后台。runserver启动本地服务浏览器打开http://127.0.0.1:8000/admin就能进入后台。为了让后台界面更直观需要在shop/admin.py里注册Product和Category模型。from django.contrib import admin from .models import Category, Product, CartItem admin.register(Category) class CategoryAdmin(admin.ModelAdmin): list_display [id, name, parent] search_fields [name] admin.register(Product) class ProductAdmin(admin.ModelAdmin): list_display [id, code, title, price, stock, is_active, created_at] list_filter [is_active, category] search_fields [code, title] list_editable [price, stock, is_active]admin.ModelAdmin中的list_display指定列表页显示的字段list_filter在右侧生成筛选栏search_fields生成搜索框。最实用的是list_editable它让你在列表页直接改价格、库存和上下架状态不需要点进详情页。这个功能对电商运营来说非常友好。后台录入两条商品数据后前台商品页用一个简单的ListView就能跑通。下面是shop/views.py的写法。from django.views.generic import ListView from .models import Product class ProductListView(ListView): model Product template_name shop/product_list.html context_object_name products paginate_by 12 def get_queryset(self): return Product.objects.filter(is_activeTrue).select_related(category)get_queryset里过滤了is_activeTrue保证下架商品不出现在前台select_related把category一次查出来避免模板里访问product.category时产生N1查询。paginate_by12设置每页12个商品适合大多数商品网格布局。模板里只需要写一个循环就能渲染商品列表。到这里EcommerceWbsite已经算能跑了后台能录商品前台能看商品。接下来真正复杂的是购物车和下单流程。4. 购物车与订单核心业务代码怎么落4.1 购物车Session方案还是Redis方案购物车是电商网站第一个拦路虎。问题不复杂但在方案选择上很多人纠结。最常见的两种做法一种是匿名购物车把购物车数据存在Session里另一种是登录购物车把数据存在数据库表里。EcommerceWbsite的CartItem模型用的是数据库方案但它默认只服务于登录用户。对于匿名用户我一般用Session做一份临时购物车等用户登录后再把Session里的数据合并到CartItem表。这套逻辑看起来繁琐但能覆盖绝大多数场景。Session方案的优点是无需额外状态、天然隔离每个浏览器缺点是服务重启或者会话过期后购物车丢失。Redis方案把购物车移到缓存层可以用Hash结构存userId - {productId: quantity}支持更长的过期时间也方便多端同步。我的建议是在小规模项目里先用Session 数据库表结合的方案别一上来就上Redis。理由很直接如果商品数量只有几十个用户在短时间内不会频繁跨设备操作Session方案足够。等出现跨端购物车同步需求、或者购物车数据需要被运营后台统计分析时再迁移到Redis。Django本身不限制你选择哪种但Session方案写起来更快。4.2 下单与库存扣减事务和锁下单是所有电商业务里最容易出错的地方。错误最典型的表现是超卖两个用户同时下单商品库存只剩1件结果两笔订单都创建成功库存变成-1。原因很简单两个请求同时读到库存为1各自判断够然后各自扣减。解决办法有两个层次原子操作和行锁。下面这段代码是EcommerceWbsite下单逻辑的核心使用事务包裹并用select_for_update锁住商品行。from django.db import transaction from django.shortcuts import get_object_or_404 from django.http import JsonResponse from .models import Product, Order, OrderItem transaction.atomic def create_order(request): if request.method ! POST: return JsonResponse({error: 仅支持POST}, status405) cart_items request.user.cart_items.filter(selectedTrue) if not cart_items.exists(): return JsonResponse({error: 购物车为空}, status400) total_amount 0 order_items [] # 逐个商品加锁并扣减库存 for cart_item in cart_items: product Product.objects.select_for_update().get(pkcart_item.product_id) if product.stock cart_item.quantity: return JsonResponse({error: f{product.title}库存不足}, status400) product.stock - cart_item.quantity product.sales_count cart_item.quantity product.save() snapshot { code: product.code, title: product.title, } subtotal product.price * cart_item.quantity total_amount subtotal order_items.append(OrderItem( productproduct, product_snapshotstr(snapshot), price_snapshotproduct.price, quantitycart_item.quantity, subtotalsubtotal, )) # 状态在Python端维护后面更新 # 创建订单主表 order Order.objects.create( order_nogenerate_order_no(request.user.id), userrequest.user, total_amounttotal_amount, statuspending, address_snapshotrequest.POST.get(address, ), ) for item in order_items: item.order order OrderItem.objects.bulk_create(order_items) # 清空已勾选的购物车 cart_items.delete() return JsonResponse({order_no: order.order_no, status: created}, status201)重点解释select_for_update。它会在数据库层面锁住查询到的商品行直到当前事务提交或回滚。这样当第二个请求进入时它会被阻塞在读取锁上等第一个事务提交后它读到的库存已经是扣减后的值。加上transaction.atomic后任何一步出错数据库都会回滚不会出现订单创建成功但库存没扣或者库存扣了但订单失败的情况。代码里我特意把商品库存扣减放在创建订单之前这是有讲究的。现实中库存是稀缺资源先扣能尽早防止超卖如果订单创建失败事务回滚会把库存一起还原。这里的generate_order_no函数会在第6章详细展开。4.3 支付回调与订单状态流转下单创建的是pending状态等真正支付成功后需要有一个接口接收支付结果并把状态改为paid。真实接入支付时这一步要通过加签验签来验证来源但在本地模拟阶段用一个专用视图模拟回调即可。from django.views.decorators.csrf import csrf_exempt from django.http import JsonResponse from .models import Order from django.utils import timezone csrf_exempt def mock_payment_callback(request): order_no request.POST.get(order_no) paid_amount request.POST.get(paid_amount) try: order Order.objects.select_for_update().get(order_noorder_no) except Order.DoesNotExist: return JsonResponse({error: 订单不存在}, status404) if order.status ! pending: return JsonResponse({error: 订单状态不允许支付}, status400) if float(paid_amount) ! float(order.total_amount): return JsonResponse({error: 支付金额不匹配}, status400) order.status paid order.paid_at timezone.now() order.save() return JsonResponse({message: 支付成功}, status200)状态机是订单模块最容易搞乱的地方。我习惯在Order模型上加一个常量列表把所有合法状态写清楚然后用choices约束字段避免脏数据。pending、paid、shipped、completed、canceled这五个状态基本够用。canceled只能从pending流转已支付的订单要退款后才能取消退款逻辑不在本篇文章范围但它同样需要事务保护和库存回补。4.4 下单后需要异步任务吗如果订单创建后需要发短信、发邮件、做库存日志这些操作不应该放在HTTP请求线程里否则用户会等很久。常见做法是用Celery或者Django自带的后台任务但在EcommerceWbsite这种规模的项目里我建议先别引入Celery因为它会让整个项目部署复杂度上升。可以先写简单的管理命令定时扫描pending超时订单并自动取消这样对新手更友好也足够支撑早期业务。5. 避坑指南Django电商网站最常见的5个坑5.1 现象模板继承不生效页面顶部一直有块空白原因templates目录包含关系配置错最常见的是在子模板中使用{% extends base.html %}时没有在settings.py中设置DIRS或者base.html本身使用{% include %}引用了不存在的片段导致Django的模板引擎没有找到对应的父模板于是静默渲染成空白区域。解决在settings.py的TEMPLATES里设置DIRS: [BASE_DIR / templates]并把所有公共模板放进项目根目录下的templates文件夹。子模板中extends路径一定要写对比如根目录是templates/base.html子模板用{% extends base.html %}如果子模板在templates/shop/目录下路径写base.html而不是../base.html。遇到空白块时先把{% include %}的name参数逐个注释掉定位到具体是哪个片段丢失。5.2 现象后台保存商品时价格变成999.9999999原因用FloatField存价格浮点数在二进制表示下会出现精度误差虽然你在表单里输入的是199.00但Django转到数据库时可能变成199.00000000001。后台列表显示时四舍五入看不出来但对账时累加所有订单金额就会差一分钱。解决把商品价格、订单总额、小计金额全部改成DecimalField设置max_digits10, decimal_places2。如果数据库已经建了FloatField先用makemigrations生成数据迁移再用Python脚本遍历把浮点数转为字符串并保留两位小数。这是一场血泪经验越早修复成本越低。5.3 现象订单取消后库存一直没回来原因取消订单时只改了Order.statuscanceled没有做库存回补。商品表里的库存字段是下单时扣减的取消后需要把原数量加回去否则可出售库存越来越少。解决在取消订单视图里用事务包裹状态更新和库存恢复逻辑。对每个OrderItem关联的商品执行product.stock item.quantity然后保存。注意要先查询原订单的明细用price_snapshot而不是实时价格。如果订单部分商品已发货只允许取消未发货部分否则库存回补会不准。5.4 现象输入中文商品名称数据库报Illegal mix of collations原因MySQL默认字符集可能是latin1或者建库时没有指定utf8mb4导致中文数据写入失败连带Django的迁移也可能中断。解决建库时执行CREATE DATABASE ecommerce CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;在settings.py的连接配置里加上OPTIONS: {charset: utf8mb4}。如果已经建了库可以用ALTER DATABASE和ALTER TABLE修改字符集。这个坑不只在Django任何ORM连接MySQL都会遇到。5.5 现象高并发下单后库存显示负数原因下单视图没有加锁两个请求同时读到stock1都执行了product.stock - 1最后数据库里的库存被更新两次变成-1。即使代码里写了if判断也只是在Python层面判断无法阻止并发读取。解决使用select_for_update()锁定商品行并在同一事务里完成扣减。更稳妥的做法是在扣减时使用条件更新Product.objects.filter(pkpk, stock__gtequantity).update(stockF(stock) - quantity)然后检查更新是否返回1行。如果返回0说明库存不足。这个方法比锁行更轻量但需要配合事务保证订单创建原子性。我倾向于两者一起用锁行清晰条件更新兜底。6. 一个进阶技巧订单号生成器与对账的后悔药订单号不是随便一个自增ID就行。自增ID泄露的是业务量而且联调时不同环境订单号可能重复。EcommerceWbsite里我用一个独立的生成器来生成order_no它让每笔订单都有一个肉眼可读、可回溯的编号。import time import random import hashlib class OrderNumberGenerator: staticmethod def generate(user_id): ts int(time.time() * 1000) rand random.randint(1000, 9999) raw f{user_id}{ts}{rand} digest hashlib.md5(raw.encode(utf-8)).hexdigest()[:6].upper() return f{time.strftime(%Y%m%d%H%M%S)}{user_id:04d}{digest}这个生成器的逻辑是日期时间精确到秒加用户ID的4位补零再加一段MD5的6位短哈希。你要的是唯一性和可读性的平衡。日期时间让对账时一眼看出下单时间用户ID让售后时快速定位下单人随机哈希用来降低极端情况下的并发碰撞。对于日单量几万的系统几乎不会重复如果真担心可以在Order表上建unique约束插入失败时捕获IntegrityError重新生成。我上一套系统直接用数据库自增ID当订单号一次联调中运营拿着两个环境的数据对比发现有相同单号查了半天才发现是两边各自从1开始计数。后来全部切换到生成器并在表层面加唯一约束。这种感觉就像提前留好了后悔药——任何时刻你想从订单号反查是哪个用户、在哪个时间下的单都能做到。你不需要依赖数据库的隐藏ID也不用担心被外部猜出销量。建议你在EcommerceWbsite刚起步时就做好这一层设计后面迁移成本会小很多。这个项目做下来最深的教训就是电商网站的很多问题不是框架不会用而是模型设计和数据约束没想清楚。一次下单事务搞定库存和订单一次订单号生成器搞定对账和排查这些细节才是让一个Django电商网站真正能撑到上线的东西。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑