1. 项目定位与技术栈的真实组合方式
第一次看到"Python+Django+SSM化妆品销售系统"这个标题时,我自己也愣了一下。Django是Python生态里的全栈Web框架,SSM对应的却是Spring MVC、Spring、MyBatis这套Java技术栈,这两组词放在一起,严格来说是不成立的。但如果你在资源站、课设和毕设相关文章里搜过,就会发现这种标题特别多,发布方只是为了把热门框架关键词都挂上去,让同一个仓库在搜索里同时命中两类用户。真正动手做项目时,二选一即可:要么纯Python+Django,要么纯Java+SSM,两边都能把电商业务闭环做出来。
这篇文章围绕的是Python+Django实现的那一版。功能覆盖前台商品浏览、搜索筛选、SKU选择、购物车、结算下单、模拟支付、订单跟踪,以及后台的会员、商品、库存、订单处理和基础数据统计。对选毕设题目、刚学完Django想做综合项目、或者想给垂直品类搭一套商城底子的同学来说,参考价值都比较大。和商业系统相比,这套项目最核心的价值不在于功能多,而在于把电商业务链路完整跑通,代码能讲清楚、能演示、能答得上原理。
"源码+LW+调试文档+讲解"这几个词也透露出项目的交付属性。LW通常是论文文档的简写,配套调试文档和讲解说明,说明这是个综合实践项目。这篇博文不展开论文怎么组稿,重点放在系统设计和代码实现思路上,尤其是化妆品这个垂直品类和通用电商平台的差异,以及Django开发里容易翻车的细节。
1.1 这套系统适合谁,能覆盖哪些场景
接触过的课设和毕设项目里,绝大多数需求就一句话:用户能注册登录、能看商品、能加购物车、能下单,管理员能管商品、能管订单。化妆品销售系统单独拿出来做,是因为它在商品属性和销售规则上有明显的垂直特征。
第一点是SKU维度复杂。一瓶粉底液可能会有五个色号、三种规格、两种质地,排列组合下来十几个SKU,每个SKU的库存和价格还不一样。如果按普通商品表一张表存所有品名,后面订单、库存、统计全都会乱套。第二点是运营后台需求重。化妆品涉及品牌、系列、功效、适用肤质、保质期、批次号等维度,后台不仅要能增删改,还要能快捷筛选、上下架、调库存。第三点是订单状态流转细。用户下单、付款、商家发货、确认收货、退款售后,每一步都有时间点和记录要求,系统里至少得有一个清晰的状态机。
从使用场景来看,这套系统适合三类人:正在选毕设题目、需要一套能讲解的电商Demo的同学;刚学完Django基础、想做一个综合项目的学习者;小团队要做垂直品类商城、先做技术验证的开发者。把这套体系吃透以后,换成图书、数码、鞋服,底层逻辑完全通用,业务层替换数据就行。
1.2 为什么演示Demo要区分"核心闭环"和"周边功能"
拿到需求先别急着建工程、写模型。我习惯先把功能分成两类:核心业务闭环和周边功能。
核心闭环指的是从商品展示到订单完成所必需的链路:注册登录、浏览商品、查看详情、加入购物车、提交订单、模拟支付、订单状态更新、后台发货。这条链路必须完整且自洽,任何一环断了,演示效果都会大打折扣。周边功能包括评论、收藏、优惠券、充值、消息通知、数据报表大屏等。这批功能属于锦上添花,一定要量力而行。如果时间只有两周,先保证核心闭环稳定,然后挑一两个周边功能做得漂亮一点,比如首页轮播推荐和热销排行,比做一堆半成品接口更能加分。
我见过不少翻车案例:代码里建了十几个app、几十张模型表,看起来功能全覆盖,结果订单状态改来改去对不上,库存和销量也不一致,答辩时一操作就露怯。先小后大、先通后全,才是做这类综合项目的稳妥路径。真要把周边功能做全,建议在核心闭环稳定之后再迭代,否则越加越容易出问题。
2. 化妆品SKU建模:为什么不能照搬普通商品表
很多同学的第一版数据表长这样:商品表里直接放商品名、价格、库存、图片、描述,所有信息平铺在一张表里。这个设计放在图书或通用小商品上勉强能跑,放到化妆品上立刻出问题。
举个具体例子。某品牌粉底液一共六个色号,每个色号分30ml和50ml两种规格,其中30ml版本又分正装和小样两种卖法。按照平铺表设计,同一款商品要拆出二十几行数据。后台看到一堆名字相似但价格库存各异的行,眼睛先就花了;更麻烦的是,用户在前台加购物车时对照"色号+规格"的维度去定位,查询条件漏一个,加到购物车里的东西就不对了。
正确做法是拆出SPU和SKU两层。SPU是商品的概念,包含标题、主图、品牌、类目、详情描述等公共信息;SKU才是实际可购买的最小库存单元,价格、库存、规格属性都挂在SKU上。用户在前台看到某个SPU,选择具体色号和规格后,实际下单的对象是一个SKU。这个结构和主流电商平台的商品模型思路一致,用在化妆品垂直商城上非常顺手。
2.1 SPU与SKU的拆分,顺带解决色号和规格的组合问题
拆分之后,规格值怎么存是第一个要决策的问题。常见方案有几种。
第一种是给SKU表加固定字段,颜色、规格、质地各加一列。优点是结构清晰,但换一个品类,字段就不够用,改表很僵硬。第二种是用JSON字段存规格键值对,比如{"色号": "02", "规格": "30ml"}。优点是灵活,新增属性不用改表结构;缺点是筛选时要用JSON查询,写起来比普通字段稍绕。第三种是维护一张规格明细表,把每个SKU的规格键值对记录成多行,用外键关联。灵活度最高,但联表变多,对新手不算友好。
在这个项目里,我推荐用Django的JSONField方案。底层走数据库的JSON类型,配合contains查询可以按规格值筛选,性能和灵活度平衡得很好。查询类似SKU.objects.filter(spec_values__contains={'色号': '02'}),对演示项目级别来说已经非常好用。JSONField还有一个好处:前端在SKU选择面板上可以直接把选中的规格对象传回来,后端存进去,省去解析多个字段的麻烦。
2.2 用Django模型把品牌、类目、商品、SKU串起来
下面把项目核心模型贴出来。注意类目用了自关联实现层级,品牌表独立,商品和SKU分开建模:
# models/product.py from django.db import models class Category(models.Model): name = models.CharField(max_length=100) parent = models.ForeignKey( 'self', null=True, blank=True, on_delete=models.CASCADE, related_name='children' ) sort_order = models.IntegerField(default=0) class Meta: ordering = ['sort_order'] def __str__(self): return self.name class Brand(models.Model): name = models.CharField(max_length=120, unique=True) logo = models.ImageField(upload_to='brands/', blank=True) description = models.TextField(blank=True) def __str__(self): return self.name class Product(models.Model): name = models.CharField(max_length=200) brand = models.ForeignKey(Brand, on_delete=models.SET_NULL, null=True, related_name='products') category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True, related_name='products') main_image = models.ImageField(upload_to='products/') detail = models.TextField(blank=True) is_active = models.BooleanField(default=True) created_at = models.DateTimeField(auto_now_add=True) def __str__(self): return self.name class SKU(models.Model): product = models.ForeignKey(Product, on_delete=models.CASCADE, related_name='skus') spec_values = models.JSONField(default=dict) price = models.DecimalField(max_digits=10, decimal_places=2) stock = models.PositiveIntegerField(default=0) sales = models.PositiveIntegerField(default=0) is_active = models.BooleanField(default=True) created_at = models.DateTimeField(auto_now_add=True) class Meta: unique_together = ('product', 'spec_values') def __str__(self): return f'{self.product.name} {self.spec_values}'这里有两个细节值得注意。
第一个是unique_together约束。它保证同一个商品下不会出现两个完全相同的规格组合,否则用户下单时,系统根本分不清他选的是哪一个,时间一长数据就乱了。要注意,JSONField参与联合唯一约束,在部分数据库版本下支持程度不一样,开发阶段就要在目标数据库上验证。
第二个是sales字段。销量是商品列表页排序的常用指标,把销量冗余在SKU上,订单完成后累加一次,比每次去订单表做聚合快得多。这是刻意设计的冗余,不是数据冗余的坏味道。
2.3 购物车和订单模型的设计要点
购物车和订单的模型同样要贴着业务走。购物车条目必须关联SKU,不能关联Product,因为同一个商品不同色号的价格和库存都不同,加购物车时就必须把SKU定死。订单条目也要关联SKU,同时快照商品名、规格描述和单价:
# models/order.py class Cart(models.Model): user = models.ForeignKey('auth.User', on_delete=models.CASCADE, related_name='carts') class CartItem(models.Model): cart = models.ForeignKey(Cart, on_delete=models.CASCADE, related_name='items') sku = models.ForeignKey(SKU, on_delete=models.CASCADE) quantity = models.PositiveIntegerField(default=1) created_at = models.DateTimeField(auto_now_add=True) class Order(models.Model): STATUS_CHOICES = [ ('PENDING_PAYMENT', '待支付'), ('PAID', '已支付'), ('SHIPPED', '已发货'), ('COMPLETED', '已完成'), ('CANCELLED', '已取消'), ] user = models.ForeignKey('auth.User', on_delete=models.PROTECT, related_name='orders') order_no = models.CharField(max_length=32, unique=True) status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='PENDING_PAYMENT') total_amount = models.DecimalField(max_digits=10, decimal_places=2) receiver_name = models.CharField(max_length=50) receiver_phone = models.CharField(max_length=20) receiver_address = models.CharField(max_length=200) remark = models.TextField(blank=True) created_at = models.DateTimeField(auto_now_add=True) paid_at = models.DateTimeField(null=True, blank=True) class OrderItem(models.Model): order = models.ForeignKey(Order, on_delete=models.CASCADE, related_name='items') sku = models.ForeignKey(SKU, on_delete=models.PROTECT) product_name = models.CharField(max_length=200) spec_values = models.JSONField(default=dict) unit_price = models.DecimalField(max_digits=10, decimal_places=2) quantity = models.PositiveIntegerField(default=1)订单号推荐用时间戳加随机数生成,格式类似20240601120000123456,创建订单时查重,有冲突就重新生成。收件人信息必须整体快照到订单里,不能去读用户资料的实时值。否则用户改完收货地址,历史订单的收货信息也跟着变,后面发货、对账、售后全部会乱。
3. 购物到履约的完整闭环代码实现
模型定完之后,接下来把整条业务链路串起来。我按"注册登录—商品列表—加入购物车—结算下单—支付回调—后台发货—确认收货"的顺序来讲,每一步给出关键代码和背后的业务判断。
3.1 登录、购物车和结算接口的核心逻辑
登录直接用Django自带的authenticate和login就能搞定。关键做好两件事:一是密码错误时的提示要友好,二是登录后才能访问的接口统一加上权限控制,用@login_required装饰器即可,未登录用户跳转到登录页。
购物车用一张Cart主表和CartItem明细表。加入购物车的接口,业务逻辑只有三步:根据登录用户找到或创建购物车;根据SKU id和数量创建或更新购物车条目;返回购物车条目数和总金额。这里有个容易错的地方:同一个SKU已经在购物车里时,应当是数量加一,而不是再插一条新记录。用get_or_create可以简化,但要注意已存在记录的quantity累加逻辑:
# views/cart.py from django.shortcuts import get_object_or_404 from django.http import JsonResponse from .models import Cart, CartItem, SKU def add_to_cart(request): if request.method == 'POST': sku_id = request.POST.get('sku_id') quantity = int(request.POST.get('quantity', 1)) sku = get_object_or_404(SKU, id=sku_id, is_active=True) cart, _ = Cart.objects.get_or_create(user=request.user) item, created = CartItem.objects.get_or_create( cart=cart, sku=sku, defaults={'quantity': quantity} ) if not created: item.quantity += quantity item.save() return JsonResponse({'code': 0, 'msg': '加入成功'})结算接口的逻辑稍多一点:接收购物车条目ID列表,遍历条目,校验SKU是否在售、库存是否充足,计算总金额,创建订单和订单明细,最后清空对应购物车条目。这几个步骤必须包在事务里,任何一个SKU校验失败都整体回滚,不能出现订单创建了一半、购物车却已清空的尴尬状态:
# views/order.py from django.db import transaction from django.shortcuts import get_object_or_404 from .models import Order, OrderItem, Cart, CartItem, SKU @transaction.atomic def create_order(request): cart_item_ids = request.POST.getlist('cart_item_ids') items = CartItem.objects.filter( id__in=cart_item_ids, cart__user=request.user ).select_related('sku__product') if not items: return JsonResponse({'code': 1, 'msg': '没有选中商品'}) total_amount = 0 order_items = [] for item in items: sku = item.sku if not sku.is_active: return JsonResponse({'code': 1, 'msg': f'{sku.product.name} 已下架'}) if sku.stock < item.quantity: return JsonResponse({'code': 1, 'msg': f'{sku.product.name} 库存不足'}) total_amount += sku.price * item.quantity order_items.append(OrderItem( sku=sku, product_name=sku.product.name, spec_values=sku.spec_values, unit_price=sku.price, quantity=item.quantity )) order = Order.objects.create( user=request.user, order_no=generate_order_no(), status='PENDING_PAYMENT', total_amount=total_amount, receiver_name=request.POST.get('receiver_name'), receiver_phone=request.POST.get('receiver_phone'), receiver_address=request.POST.get('receiver_address'), remark=request.POST.get('remark', '') ) OrderItem.objects.bulk_create(order_items) items.delete() return JsonResponse({'code': 0, 'order_id': order.id})生成订单号时可以把generate_order_no实现成时间戳加随机数的组合。事务内创建订单和图省事直接用items.delete()清空购物车,前提是已经在函数最外层加@transaction.atomic,这样任何一步出错都会回滚。
3.2 订单状态机:从"待支付"到"已完成"的流转细节
订单状态是整套系统的核心。我用的状态集合是待支付、已支付、已发货、已完成、已取消,五个状态覆盖常规销售流程。设计上要规定好哪些状态能迁移到哪些状态:
- 待支付 -> 已支付:支付成功
- 待支付 -> 已取消:用户主动取消或超时未支付
- 已支付 -> 已发货:后台发货
- 已发货 -> 已完成:用户确认收货
- 已支付 -> 已取消:售后场景下管理员取消
在代码层面,我给Order模型加一个transition_status方法,显式处理合法的状态迁移,不让任意赋值拆掉状态机的约束:
# models/order.py class Order(models.Model): # ...字段省略 def transition_status(self, new_status): allowed = { 'PENDING_PAYMENT': {'PAID', 'CANCELLED'}, 'PAID': {'SHIPPED', 'CANCELLED'}, 'SHIPPED': {'COMPLETED'}, } if new_status not in allowed.get(self.status, set()): raise ValueError(f'非法状态变更:{self.status} -> {new_status}') self.status = new_status self.save()状态迁移的触发点也要明确。支付回调里把待支付改成已支付,并记录支付时间;后台订单管理页面把已支付改成已发货;用户确认收货把已发货改成已完成。每个动作对应一个明确的接口或后台按钮,不要在页面里开放一个"改状态"的下拉框,让操作者随意选。真需要灵活改状态,也要在Service层走一遍校验逻辑,最大程度保证数据可靠。
3.3 库存扣减方案与防超卖处理
电商系统最容易出事的环节就是库存。常见做法有两种:下单减库存和支付减库存。下单减库存的优点是用户下单后库存即时被占用,不会出现"下单成功但一直没货"的情况;缺点是很多用户下单后不支付,产生大量无效占用。支付减库存的优点是占用少,但风险在于多人同时支付时,扣到最后库存可能变成负数。
对课设和毕设级别的项目,我建议用"下单时校验库存充足,支付确认后扣减库存"的折中方案。具体流程是:创建订单时不扣库存,但校验stock >= quantity;支付回调成功后,执行带条件的库存更新:
# services/stock.py from django.db.models import F def deduct_stock(sku_id, quantity): affected = SKU.objects.filter( id=sku_id, stock__gte=quantity ).update( stock=F('stock') - quantity ) if affected == 0: raise ValueError('库存不足')update配合F表达式在数据库层完成条件扣减,比"先select再update"更安全,能避免并发时的超卖。答辩时虽然不太可能模拟高并发,但这样写有得讲:面试官问并发超卖怎么处理,你可以从数据库条件更新这个角度给出有理有据的回答,这就是区分度所在。
订单完成后,还要同步累加SKU的sales字段。同一次支付回调里做库存扣减和销量累加,依然要包在事务里,保证两个数字要么都更新,要么都不动。
4. 后台权限与商品管理:admin体系的二次开发
Django自带的Admin后台开箱即用,但直接拿来当运营后台是不够的,至少要做三件事:自定义用户模型、划分角色权限、定制列表和表单页面。前期不做用户模型扩展,后面再改会非常痛苦,因为Django的AUTH_USER_MODEL在第一次迁移后就不能随意变更。
4.1 自定义User模型与三方角色的权限划分
一个化妆品销售系统里,后台至少有三类角色:超级管理员、运营人员、仓库发货人员。如果算上用户端,其实是四方。
在项目初期就使用AbstractUser扩展用户模型,增加role字段,比默认的User表合理得多。好处是权限判断时通过request.user.role就能分支,后续想加角色也不用重构表:
# models/user.py from django.contrib.auth.models import AbstractUser from django.db import models class User(AbstractUser): ROLE_CHOICES = [ ('ADMIN', '超级管理员'), ('OPERATOR', '运营人员'), ('WAREHOUSE', '仓库人员'), ('CUSTOMER', '普通用户'), ] role = models.CharField(max_length=20, choices=ROLE_CHOICES, default='CUSTOMER')在settings.py里指定AUTH_USER_MODEL = 'accounts.User',注意这个配置必须在第一次迁移前设置好。角色权限的大致划分可以参考下面这张表:
| 角色 | 可操作范围 |
|---|---|
| 超级管理员 | 全部权限,包括用户管理、角色分配、数据统计 |
| 运营人员 | 商品上下架、SKU价格库存维护、活动与推荐位配置 |
| 仓库人员 | 查看待发货订单、执行发货、回填物流单号 |
| 普通用户 | 浏览商品、购物车、下单、确认收货、查看订单 |
权限控制建议分两层做。接口层用自定义装饰器检查角色;后台菜单层通过Admin的has_module_permission和has_change_permission控制不同角色能看到哪些模块。
# decorators.py from functools import wraps from django.http import JsonResponse def require_role(*roles): def decorator(view_func): @wraps(view_func) def wrapper(request, *args, **kwargs): if not request.user.is_authenticated: return JsonResponse({'code': 401, 'msg': '未登录'}) if request.user.role not in roles and not request.user.is_superuser: return JsonResponse({'code': 403, 'msg': '无权限'}) return view_func(request, *args, **kwargs) return wrapper return decorator仓库发货接口可以这样用:@require_role('WAREHOUSE', 'ADMIN')。运营账号调过来,直接返回无权限,把权限边界挡在视图层前面,清晰又安全。
4.2 让Django Admin真正能用的改造点
直接注册模型后,后台的商品列表通常每一行只显示一个SPU,点进去才能看到SKU。这个交互对运营来说效率太低,一个商品十来个SKU,要层层点开才能看价格库存。比较推荐的改法是把SKU做成TabularInline,在商品编辑页直接维护所有SKU:
# admin.py from django.contrib import admin from .models import Product, SKU, Brand, Category class SKUInline(admin.TabularInline): model = SKU extra = 0 fields = ['spec_values', 'price', 'stock', 'sales', 'is_active'] @admin.register(Product) class ProductAdmin(admin.ModelAdmin): list_display = ['id', 'name', 'brand', 'category', 'is_active', 'created_at'] list_filter = ['brand', 'category', 'is_active'] search_fields = ['name'] inlines = [SKUInline]列表页加上品牌、类目筛选和名称搜索,后台体验立刻不一样。订单页面也要定制:默认列表要显示订单号、用户、金额、状态、下单时间。不建议直接使用list_editable去改状态,因为绕过了transition_status校验。我的做法是列表页只做展示,状态操作放到独立接口里,后台页面通过按钮调用接口完成发货等动作。
另一个容易被忽略的改造点是图片上传。ImageField上传后默认落在MEDIA_ROOT目录,开发阶段直接可用。但MEDIA_URL和STATIC_URL必须区分开,否则上线后图片和静态文件互相干扰。商品图片的命名建议加上时间戳或随机前缀,防止同名文件互相覆盖,后台维护时图片丢了很难排查是URL问题还是文件名问题。
5. 联调、部署与常见坑位排查
开发环境把功能跑通只是第一步。实际项目里,从本地联调到部署上线,会踩掉很多显性的坑。下面按问题类型梳理,每一条都来自实际项目里的教训。
5.1 图片上传和静态文件的坑
Django项目里,settings.py需要正确配置媒体和静态文件:
MEDIA_URL = '/media/' MEDIA_ROOT = BASE_DIR / 'media' STATIC_URL = '/static/' STATIC_ROOT = BASE_DIR / 'staticfiles'本地调试时,要让上传的图片能被访问,还需要在urls.py里补一段MEDIA服务配置。很多同学漏掉这段,后台明明显示上传成功,前台图片却打不开,排查半天发现只是URL路由没配:
# config/urls.py from django.conf import settings from django.conf.urls.static import static urlpatterns = [ # ...其他路由 ] if settings.DEBUG: urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)部署阶段,如果用Nginx,/media/和/static/这些静态请求通常由Nginx直接处理,Django只负责动态逻辑。图片目录的写权限经常会出问题,权限不够会导致上传失败,而且后台报错往往不直观。我的做法是上传前先在前端校验图片大小,后端再做扩展名白名单过滤,单张图片限制在5MB以内,既不担心磁盘膨胀,也防止有人上传超大文件拖垮服务。
5.2 支付接口模拟与回调处理
真正接微信支付或支付宝,需要商户资质和证书,演示项目里不现实。常规做法是模拟支付:前端下单后跳到模拟收银台页面,点击"确认支付",系统生成支付记录,把订单状态改成已支付。
关键点在于,回调处理不能只改状态。至少要建一张Payment表,记录支付单号、订单号、支付方式、金额、支付时间,把"支付动作"和"状态变更"分开记账,后续对账和排错才有依据:
# models/payment.py class Payment(models.Model): payment_no = models.CharField(max_length=32, unique=True) order = models.ForeignKey(Order, on_delete=models.PROTECT, related_name='payments') method = models.CharField(max_length=20, default='mock') amount = models.DecimalField(max_digits=10, decimal_places=2) status = models.CharField(max_length=20, default='SUCCESS') paid_at = models.DateTimeField(auto_now_add=True)模拟支付回调中,先用select_for_update锁住订单行,再做状态校验和扣库存。锁行的目的是防止支付回调与用户手动取消订单并发执行时产生脏状态。演示项目虽然很少遇到并发,但这个写法不复杂,写上去就是专业细节。
5.3 Windows/Linux两套环境的部署差异
很多同学本地用Windows开发,第一次往Linux服务器上部署就各种报错。最实用的建议是:从第一天起就用虚拟环境隔离依赖,不管是venv还是pipenv都行。依赖列表用requirements.txt固定版本,pip freeze > requirements.txt生成后,换到服务器上执行pip install -r requirements.txt就能复现环境。
Django部署到Linux时,SECRET_KEY不要硬编码在代码里,用环境变量或配置文件读取;DEBUG必须设成False,否则一行异常堆栈就会把服务器目录结构泄露给访问者。数据库方面,演示项目用SQLite也能跑,但如果你要展示并发处理和复杂查询,建议直接用MySQL或PostgreSQL,settings.py把数据库配置改为环境变量读取,本地和服务器各用一套.env文件。
进程管理上,Gunicorn加Nginx是Django最经典的组合。一个简化的启动方式是这样:
gunicorn config.wsgi:application \ --bind 0.0.0.0:8000 \ --workers 3 \ --timeout 60Nginx配置里把/static/和/media/指向对应目录,其余请求反代到8000端口。部署完成后要重点检查三项:静态文件是否执行过collectstatic、媒体目录权限是否正确、数据库连接是否正常。这三个点都通过,页面基本就能访问了。
5.4 答辩演示时要准备的测试数据与演示脚本
功能全都做完了,答辩环节绝不能临时在界面上乱点。很多项目功能没毛病,却在演示时因为数据太丑或者流程不连贯丢分。这里分享一套我常用的准备方案。
测试数据准备两类。一类是海量示例数据:20个品牌、50个SPU、每个SPU配3到5个SKU,保证列表页有翻页效果、筛选有条件可选。另一类是剧本数据:一个会员账号、一个运营账号、一个仓库账号,每个账号对应一套预定的操作路径。数据可以通过管理后台手动录,也可以写一个独立的初始化脚本一次性导入。
演示脚本我按四步走:
第一步,用普通用户账号演示逛店。从首页搜索"口红",筛选某个品牌,点进详情页,切换色号和规格,加入购物车,再买一件护肤品类商品,顺便演示一下购物车里同一SKU数量叠加的效果。
第二步,结算下单,填写收货地址,走模拟支付,支付成功后展示订单从待支付变成已支付。
第三步,切到仓库账号,进后台看到待发货订单,点发货并回填快递单号;切回用户端刷新订单,状态变成已发货。
第四步,确认收货,订单变成已完成;再回到管理后台看商品销量和库存变化,顺便演示库存不足时下单被拦截的效果。
每走一步,对应讲解一下代码里这一块是怎么实现的。这样的演示节奏紧凑、逻辑闭环,比临时在界面上乱点效果好太多。
还有个细节,演示前把浏览器缓存清理掉,提前在无痕窗口里登录好三个账号,避免演示时卡在登录环节。这个看似不起眼的小准备,关键时刻能省下不少尴尬时间。
最后说点我自己的体会。第一次搭这类系统时,我也走过弯路。一开始照着大型电商平台做全功能,数据表越设计越复杂,写了两周连核心下单流程都没跑通。后来把范围砍到最小可闭环版本,先让用户能完成一次完整购买,再逐步加筛选、加后台优化、加统计功能,项目反而快速推进下来了。
多讲一个细节:项目里的"化妆品"不是简单换几个商品名。SKU规格、肤质推荐、保质期这些维度的建模,才是真正花心思的地方。把这套模型吃透,下次换一个垂直品类,只需要替换数据内容,代码骨架可以直接复用。
如果你准备做类似项目,我的建议是三句话:先画清楚业务闭环图,再写模型层,然后再补接口和页面。模型定错,后面改起来成本极高;模型一旦合理,前后端就是顺水推舟的事。这篇内容的代码片段都是从项目中截取的,完整项目里还包含前端页面、初始化脚本、配套文档和调试说明。照着上面的思路搭起来,两周内出活很有把握。