社区团购小程序开发复盘:Python + Django 从零到上线全记录
最近刚完成了一个社区团购微信小程序的从零开发到上线,技术栈选了 Python + Django + 微信小程序原生框架。整套东西做下来,踩了不少坑,也沉淀了不少经验,趁着热乎劲儿把完整过程做个复盘,从业务设计、数据库建模、后端接口开发,到小程序端实现、支付对接、生产部署,再到线上问题的排查,全都过一遍。
如果你正在准备做一个微信小程序电商类项目,或者想了解 Django 在实际项目里怎么组织代码、怎么跟小程序优雅配合,这篇内容应该能帮你省不少时间。我会尽可能把关键操作用大白话讲透,参数怎么定、代码怎么组织、坑在哪里,都说清楚。
1. 项目到底要解决什么问题
1.1 社区团购的业务本质拆解
社区团购听起来是个流行词,但拆开看,业务模型其实很清晰。它本质上是一种“预售 + 集中配送 + 自提”的零售模式:平台或团长提前一天发布商品,用户下单,平台统一采购或配货,第二天把货送到小区自提点,用户自己过去取。
这个模式和普通电商最大的区别在于三个地方:
第一,履约方式不同。普通电商是仓库发货、快递到家,社区团购是批量配送到自提点、用户自取。这意味着系统里必须有“自提点”这个实体,而且用户下单时要选择自提点,平台发货时按自提点聚合订单。
第二,商品管理方式不同。社区团购的商品往往有“当天可售、隔天清仓”的特点,商品上架时间很短,而且经常有“限量”“秒杀”性质的活动。所以商品表需要一个日期维度,比如商品属于哪一天的活动,而不是永久有效。
第三,信任关系不同。社区团购高度依赖团长这个角色,团长既是小区的推广者,也是自提点的经营者,还承担了一部分售后服务的职责。系统里团长和自提点应该是一对一绑定,而且订单归属需要记录到团长维度。
所以做系统设计之前,先别急着写代码,把这几条业务逻辑理清楚,后面所有表结构、接口设计、前端页面都会围绕这几条线展开。
1.2 为什么选 Django 而不是其他框架
选 Django 的原因很实际。
项目要求是 Python,Python 生态里做 Web 后端的主流选择无非是 Django、Flask、FastAPI。Flask 灵活但东西都要自己搭,FastAPI 适合接口纯后端项目,而 Django 自带 Admin 后台、ORM、迁移工具、认证系统,对“需要运营后台管理商品和订单”的团购项目来说,真的太合适了。
Django Admin 可以直接当运营后台用,商品管理、订单查看、团长审核这些操作,开发阶段甚至可以直接在 Admin 里完成,不需要额外开发一套后台管理界面。对于小团队或者个人开发者来说,这能省出至少两周的开发量。
还有一点是 Django ORM 的迁移机制非常成熟。小程序端和后端接口的开发往往是同步推进的,字段说加就加、说改就改,python manage.py makemigrations 加 migrate 两步走,数据库同步非常方便。用原生 SQL 的话,这种频繁改动会很痛苦。
另外选 Django 其实还考虑了后续扩展的余地。社区团购跑起来之后,大概率会加积分系统、优惠券、会员等级、分销返利这些功能。Django 的 APP 模块化结构天然适合这种渐进式扩展,一个功能一个 APP,互不干扰。
1.3 整体功能模块的划分
开始写代码之前,我把系统拆成了这几个核心模块:
- 用户模块:微信登录授权、用户信息维护、地址管理,区分普通用户和团长角色
- 商品模块:商品分类、商品发布、库存管理、上下架状态,按日期维度组织
- 订单模块:购物车、下单、订单状态流转、订单列表、订单详情
- 支付模块:微信支付下单、支付回调处理、退款
- 团长/自提点模块:团长申请与审核、自提点管理,跟用户绑定
- 运营后台:基于 Django Admin 实现,管理商品、订单、用户、团长审核
其中订单状态流转是最容易写乱的,先设计好状态机再写代码。我的订单状态是这样的:待支付(用户下单未付款)、已支付(回调成功)、配送中(平台发货)、待自提(到达自提点)、已完成(用户确认或超时自动确认)、已取消(超时未支付或用户主动取消)、售后中、已退款。
这个状态机设计是整个订单系统的骨架,后面做支付回调、订单列表筛选、后台订单管理,全都围着它转。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 数据库设计和 Django 项目骨架搭建
2.1 核心数据模型的设计思路
数据模型是系统的地基,这块一开始要是设计错了,后面改起来真的会想哭。我最终的表结构是这样的,给大家参考。
用户表直接复用 Django 自带的 User 模型,然后通过一个 OneToOne 的 Profile 表扩展微信用户信息。微信小程序登录后拿到的 openid 是用户在微信生态里的唯一标识,这个字段必须有,而且建议加唯一索引。注意一点,同一个微信号在微信开放平台、公众号、小程序下的 openid 是不同的,跨平台识别需要 unionid,但社区团购这种场景基本都是小程序内闭环,用 openid 就够了。
商品表要注意的地方是增加了一个 activity_date 字段,表示这个商品属于哪一天的团购活动。这样做的好处是,用户端默认只显示今天的活动商品,运营后台可以提前配置后面几天的商品,到点自动切换。
关键模型大概是这样的(精简版):
python复制from django.db import models
from django.contrib.auth.models import User
class UserProfile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile')
openid = models.CharField(max_length=64, unique=True, db_index=True)
nickname = models.CharField(max_length=64, blank=True)
avatar_url = models.CharField(max_length=255, blank=True)
phone = models.CharField(max_length=20, blank=True)
role = models.CharField(max_length=10, choices=(
('user', '普通用户'),
('leader', '团长'),
), default='user')
created_at = models.DateTimeField(auto_now_add=True)
class Category(models.Model):
name = models.CharField(max_length=32)
sort_order = models.IntegerField(default=0)
is_active = models.BooleanField(default=True)
class Product(models.Model):
category = models.ForeignKey(Category, on_delete=models.CASCADE, related_name='products')
name = models.CharField(max_length=128)
desc = models.TextField(blank=True)
image = models.CharField(max_length=255)
price = models.DecimalField(max_digits=10, decimal_places=2)
original_price = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True)
stock = models.IntegerField(default=0)
sales = models.IntegerField(default=0)
activity_date = models.DateField(db_index=True)
is_active = models.BooleanField(default=True)
created_at = models.DateTimeField(auto_now_add=True)
class PickerPoint(models.Model):
name = models.CharField(max_length=64)
address = models.CharField(max_length=255)
leader = models.OneToOneField(User, on_delete=models.CASCADE, related_name='picker_point')
phone = models.CharField(max_length=20)
lat = models.FloatField(null=True, blank=True)
lng = models.FloatField(null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
class Order(models.Model):
order_no = models.CharField(max_length=32, unique=True)
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='orders')
picker_point = models.ForeignKey(PickerPoint, on_delete=models.SET_NULL, null=True)
total_amount = models.DecimalField(max_digits=10, decimal_places=2)
status = models.CharField(max_length=20, choices=(
('pending_payment', '待支付'),
('paid', '已支付'),
('shipping', '配送中'),
('pending_pickup', '待自提'),
('completed', '已完成'),
('cancelled', '已取消'),
('refunding', '退款中'),
('refunded', '已退款'),
), default='pending_payment')
transaction_id = models.CharField(max_length=64, blank=True)
remark = models.CharField(max_length=255, 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')
product = models.ForeignKey(Product, on_delete=models.CASCADE)
product_name = models.CharField(max_length=128)
product_image = models.CharField(max_length=255)
price = models.DecimalField(max_digits=10, decimal_places=2)
quantity = models.IntegerField(default=1)
订单表里有一个细节值得注意:OrderItem 里我冗余存了 product_name 和 product_image,而不是下单时通过外键去关联商品表。为什么要这么做?因为商品表的数据是可能变的,如果运营把商品改名了或者删了,历史订单里的商品信息也会跟着变,这在订单系统里是不能接受的。所以要在订单项里做一个快照,记
