1. 项目背景与核心需求
在移动互联网时代,多商家入驻的网上商城小程序已经成为零售行业的主流形态。这类平台需要同时满足商家管理、商品展示、订单处理、支付结算等复杂需求,而Python凭借其丰富的Web开发框架和高效的开发效率,成为构建此类系统的理想选择。
我去年接手过一个连锁超市集团的线上商城改造项目,他们原有的PHP系统已经无法支撑200多家门店的并发运营。通过调研发现,Python+Django的组合在以下方面具有显著优势:
- 自带强大的ORM系统,轻松处理多商家数据隔离
- 完善的Admin后台,降低商家管理门槛
- 丰富的第三方支付接口集成
- 与微信小程序API的无缝对接能力
2. 技术架构设计
2.1 整体架构方案
经过多次技术验证,我们最终确定的架构方案如下:
code复制前端:微信小程序 + H5备用页面
后端:Python 3.8 + Django 3.2
数据库:MySQL 8.0(主从分离)
缓存:Redis 6.x
消息队列:RabbitMQ
文件存储:阿里云OSS
这个组合在实测中表现稳定,单服务器可支撑5000+的日活用户量。特别要说明的是,我们选择Django而非Flask的主要考虑是:
- Django自带的auth权限系统天然支持多租户隔离
- Admin后台可快速生成商家管理界面
- 完善的中间件机制便于实现全局业务逻辑
2.2 多商家数据隔离方案
这是整个系统的核心难点。我们采用了"数据库级别隔离+逻辑隔离"的混合方案:
python复制# 商家模型定义
class Merchant(models.Model):
name = models.CharField(max_length=100)
subdomain = models.CharField(max_length=50, unique=True)
is_active = models.BooleanField(default=True)
# 商品模型通过外键关联商家
class Product(models.Model):
merchant = models.ForeignKey(Merchant, on_delete=models.CASCADE)
name = models.CharField(max_length=200)
price = models.DecimalField(max_digits=10, decimal_places=2)
# 中间件实现子域名路由
class MerchantMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
host = request.get_host().split(':')[0]
subdomain = host.split('.')[0]
try:
merchant = Merchant.objects.get(subdomain=subdomain)
request.merchant = merchant
except Merchant.DoesNotExist:
return HttpResponseNotFound()
return self.get_response(request)
重要提示:在实际部署时,需要在Nginx配置中添加通配符子域名解析,例如:
code复制server_name *.yourdomain.com;
3. 微信小程序深度集成
3.1 小程序登录流程优化
传统的小程序登录流程存在token过期的问题,我们实现了双token机制:
python复制# views.py
def wx_login(request):
code = request.POST.get('code')
# 1. 通过code获取openid
wx_data = requests.get(
f'https://api.weixin.qq.com/sns/jscode2session?appid={APPID}&secret={SECRET}&js_code={code}&grant_type=authorization_code'
).json()
# 2. 生成双token
access_token = generate_token(wx_data['openid'], expires_in=7200) # 2小时过期
refresh_token = generate_token(wx_data['openid'], expires_in=2592000) # 30天过期
# 3. 返回客户端
return JsonResponse({
'access_token': access_token,
'refresh_token': refresh_token,
'expires_in': 7200
})
# 刷新token的接口
def refresh_token(request):
refresh_token = request.POST.get('refresh_token')
try:
payload = jwt.decode(refresh_token, SECRET_KEY, algorithms=['HS256'])
new_access_token = generate_token(payload['openid'], expires_in=7200)
return JsonResponse({'access_token': new_access_token})
except jwt.ExpiredSignatureError:
return JsonResponse({'error': 'refresh_token expired'}, status=401)
3.2 小程序性能优化实战
针对小程序发热发烫的问题,我们通过以下措施显著改善了性能:
- 图片懒加载优化:
python复制# 商品列表接口改造
def product_list(request):
page = int(request.GET.get('page', 1))
per_page = 10
products = Product.objects.filter(
merchant=request.merchant
).select_related('category')[per_page*(page-1):per_page*page]
# 首屏加载高清图,后续加载缩略图
if page == 1:
image_field = 'image_hd'
else:
image_field = 'image_thumb'
result = [{
'id': p.id,
'name': p.name,
'price': str(p.price),
'image': getattr(p, image_field).url
} for p in products]
return JsonResponse({'data': result})
- 接口数据缓存策略:
python复制from django.core.cache import cache
def get_categories(request):
cache_key = f'merchant_{request.merchant.id}_categories'
data = cache.get(cache_key)
if not data:
categories = Category.objects.filter(merchant=request.merchant)
data = serialize_categories(categories)
cache.set(cache_key, data, timeout=3600) # 缓存1小时
return JsonResponse({'data': data})
- WebSocket实时通知:
python复制# consumers.py
class OrderConsumer(WebsocketConsumer):
def connect(self):
self.user = self.scope['user']
self.group_name = f'user_{self.user.id}'
async_to_sync(self.channel_layer.group_add)(
self.group_name, self.channel_name
)
self.accept()
def disconnect(self, close_code):
async_to_sync(self.channel_layer.group_discard)(
self.group_name, self.channel_name
)
def order_update(self, event):
self.send(text_data=json.dumps({
'type': 'order.update',
'data': event['data']
}))
# views.py
def order_paid(request, order_id):
order = get_object_or_404(Order, id=order_id)
order.status = 'paid'
order.save()
# 通知前端
channel_layer = get_channel_layer()
async_to_sync(channel_layer.group_send)(
f'user_{order.user.id}',
{
'type': 'order.update',
'data': {
'order_id': order.id,
'status': 'paid'
}
}
)
return JsonResponse({'status': 'success'})
4. 商家后台管理系统
4.1 基于Django Admin的深度定制
我们扩展了默认的Django Admin界面,使其更适合多商家场景:
python复制# admin.py
class ProductAdmin(admin.ModelAdmin):
list_display = ('name', 'category', 'price', 'stock')
list_filter = ('category', 'is_featured')
search_fields = ('name', 'description')
def get_queryset(self, request):
qs = super().get_queryset(request)
if not request.user.is_superuser:
return qs.filter(merchant=request.user.merchant)
return qs
def save_model(self, request, obj, form, change):
if not change: # 新增时自动关联当前商家
obj.merchant = request.user.merchant
super().save_model(request, obj, form, change)
admin.site.register(Product, ProductAdmin)
4.2 商家数据看板实现
使用Chart.js为每个商家生成实时数据可视化:
python复制# views.py
def merchant_dashboard(request):
today = timezone.now().date()
orders_today = Order.objects.filter(
merchant=request.merchant,
created_at__date=today
).count()
revenue_today = Order.objects.filter(
merchant=request.merchant,
created_at__date=today,
status='completed'
).aggregate(total=Sum('total_amount'))['total'] or 0
# 最近7天数据
date_list = [today - timedelta(days=i) for i in range(6, -1, -1)]
daily_data = []
for day in date_list:
count = Order.objects.filter(
merchant=request.merchant,
created_at__date=day
).count()
daily_data.append(count)
return render(request, 'dashboard.html', {
'orders_today': orders_today,
'revenue_today': revenue_today,
'date_labels': [d.strftime('%m-%d') for d in date_list],
'daily_data': daily_data
})
5. 支付与结算系统
5.1 多支付渠道集成
我们抽象了支付接口,支持微信支付、支付宝等多家支付平台:
python复制# payments.py
class PaymentGateway:
@classmethod
def create_order(cls, merchant, amount, description):
if merchant.payment_provider == 'wechat':
return WechatPayment.create_order(
merchant.wechat_config,
amount,
description
)
elif merchant.payment_provider == 'alipay':
return AlipayPayment.create_order(
merchant.alipay_config,
amount,
description
)
raise ValueError('Unsupported payment provider')
# 微信支付实现
class WechatPayment:
@staticmethod
def create_order(config, amount, description):
# 统一下单接口
params = {
'appid': config['appid'],
'mch_id': config['mch_id'],
'nonce_str': generate_nonce(),
'body': description,
'out_trade_no': generate_trade_no(),
'total_fee': int(amount * 100), # 转为分
'spbill_create_ip': get_client_ip(),
'notify_url': config['notify_url'],
'trade_type': 'JSAPI'
}
params['sign'] = generate_sign(params, config['key'])
response = requests.post(
'https://api.mch.weixin.qq.com/pay/unifiedorder',
data=dict_to_xml(params)
)
result = xml_to_dict(response.content)
if result['return_code'] == 'SUCCESS':
return {
'payment_id': params['out_trade_no'],
'prepay_id': result['prepay_id'],
'params': {
'appId': config['appid'],
'timeStamp': str(int(time.time())),
'nonceStr': params['nonce_str'],
'package': f'prepay_id={result["prepay_id"]}',
'signType': 'MD5'
}
}
raise PaymentError(result['return_msg'])
5.2 自动分账与结算
实现T+1自动结算到商家账户:
python复制# tasks.py
@app.task
def daily_settlement():
yesterday = timezone.now() - timedelta(days=1)
merchants = Merchant.objects.filter(is_active=True)
for merchant in merchants:
orders = Order.objects.filter(
merchant=merchant,
status='completed',
created_at__date=yesterday.date(),
settlement_status='pending'
)
total_amount = orders.aggregate(
total=Sum('total_amount')
)['total'] or 0
if total_amount > 0:
fee = calculate_platform_fee(total_amount)
settlement_amount = total_amount - fee
# 调用支付平台转账接口
try:
transfer_result = PaymentGateway.transfer(
merchant,
settlement_amount,
f'结算{yesterday.strftime("%Y%m%d")}'
)
if transfer_result['status'] == 'success':
orders.update(settlement_status='completed')
SettlementRecord.objects.create(
merchant=merchant,
settlement_date=yesterday.date(),
total_amount=total_amount,
fee_amount=fee,
settlement_amount=settlement_amount,
transaction_id=transfer_result['transaction_id']
)
except Exception as e:
logger.error(f'Settlement failed for {merchant}: {str(e)}')
6. 部署与运维实战
6.1 生产环境部署方案
我们使用Docker Compose进行容器化部署:
dockerfile复制# docker-compose.prod.yml
version: '3.8'
services:
web:
build: .
command: gunicorn config.wsgi:application --bind 0.0.0.0:8000
volumes:
- static:/app/static
- media:/app/media
environment:
- DJANGO_SETTINGS_MODULE=config.settings.production
depends_on:
- redis
- db
networks:
- backend
db:
image: mysql:8.0
volumes:
- db_data:/var/lib/mysql
environment:
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
MYSQL_DATABASE: ${DB_NAME}
MYSQL_USER: ${DB_USER}
MYSQL_PASSWORD: ${DB_PASSWORD}
networks:
- backend
redis:
image: redis:6-alpine
volumes:
- redis_data:/data
networks:
- backend
volumes:
db_data:
redis_data:
static:
media:
networks:
backend:
6.2 性能监控与优化
使用Prometheus+Grafana搭建监控系统:
python复制# monitoring.py
from prometheus_client import start_http_server, Counter, Gauge
# 定义指标
REQUEST_COUNT = Counter(
'django_requests_total',
'Total requests count',
['method', 'endpoint', 'status']
)
RESPONSE_TIME = Gauge(
'django_response_time_seconds',
'Response time in seconds',
['endpoint']
)
# 中间件实现
class MetricsMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
start_time = time.time()
response = self.get_response(request)
path = request.path
if path.startswith('/api/'):
endpoint = path.split('/')[2]
else:
endpoint = path
REQUEST_COUNT.labels(
method=request.method,
endpoint=endpoint,
status=response.status_code
).inc()
RESPONSE_TIME.labels(
endpoint=endpoint
).set(time.time() - start_time)
return response
7. 安全防护措施
7.1 常见攻击防护
- XSS防护:
python复制# settings.py
MIDDLEWARE = [
...
'django.middleware.security.SecurityMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
]
# 模板层自动转义
{{ user_input|escape }}
# 或者使用mark_safe谨慎处理
from django.utils.safestring import mark_safe
safe_content = mark_safe('<b>Trusted content</b>')
- CSRF防护:
python复制# 前端axios配置
axios.defaults.xsrfCookieName = 'csrftoken'
axios.defaults.xsrfHeaderName = 'X-CSRFToken'
# 或者使用@csrf_exempt谨慎豁免特定接口
from django.views.decorators.csrf import csrf_exempt
@csrf_exempt
def wechat_pay_notify(request):
# 验证签名逻辑
...
7.2 数据安全策略
- 敏感数据加密:
python复制from cryptography.fernet import Fernet
class EncryptionService:
def __init__(self):
self.key = settings.ENCRYPTION_KEY
self.cipher = Fernet(self.key)
def encrypt(self, text):
return self.cipher.encrypt(text.encode()).decode()
def decrypt(self, encrypted_text):
return self.cipher.decrypt(encrypted_text.encode()).decode()
# 模型字段加密
class Customer(models.Model):
phone = models.CharField(max_length=100)
def set_phone(self, phone):
self.phone = EncryptionService().encrypt(phone)
def get_phone(self):
return EncryptionService().decrypt(self.phone)
- 定期安全审计:
python复制# management/commands/security_audit.py
from django.core.management.base import BaseCommand
from django.contrib.auth.models import User
class Command(BaseCommand):
help = 'Perform security audit'
def handle(self, *args, **options):
# 检查弱密码用户
weak_users = []
for user in User.objects.all():
if user.check_password('123456'):
weak_users.append(user.username)
if weak_users:
self.stdout.write(
self.style.ERROR(f'Weak password users: {", ".join(weak_users)}')
)
# 检查超级用户数量
superusers = User.objects.filter(is_superuser=True).count()
if superusers > 3:
self.stdout.write(
self.style.WARNING(f'Too many superusers: {superusers}')
)
8. 项目扩展与优化方向
在实际运营过程中,我们发现以下几个有价值的优化方向:
- 智能推荐系统:
python复制# 基于用户行为的简单推荐
def recommend_products(user, limit=5):
viewed = ProductView.objects.filter(
user=user
).values_list('product_id', flat=True)[:10]
if not viewed:
return Product.objects.filter(
merchant=user.merchant
).order_by('?')[:limit]
# 找出经常一起购买的商品
related = OrderItem.objects.filter(
order__user=user,
product_id__in=viewed
).values_list('product_id', flat=True)
return Product.objects.filter(
merchant=user.merchant,
id__in=related
).exclude(id__in=viewed).order_by('?')[:limit]
- 多平台小程序同步:
python复制# 抽象小程序发布接口
class MiniProgramPublisher:
def __init__(self, platform):
self.platform = platform
def upload_code(self, version, description):
if self.platform == 'wechat':
return self._upload_wechat(version, description)
elif self.platform == 'alipay':
return self._upload_alipay(version, description)
raise ValueError('Unsupported platform')
def _upload_wechat(self, version, description):
# 调用微信小程序上传接口
...
def _upload_alipay(self, version, description):
# 调用支付宝小程序上传接口
...
# 使用示例
wechat_publisher = MiniProgramPublisher('wechat')
wechat_publisher.upload_code('1.2.0', '新增商品推荐功能')
- Serverless扩展:
python复制# 使用AWS Lambda处理峰值流量
import boto3
def trigger_lambda(function_name, payload):
client = boto3.client('lambda')
response = client.invoke(
FunctionName=function_name,
InvocationType='Event',
Payload=json.dumps(payload)
)
return response
# 示例:处理促销活动的订单
def handle_flash_sale(order_data):
# 主系统处理核心逻辑
order = create_order(order_data)
# 异步处理非关键路径
trigger_lambda('send-notification', {
'user_id': order.user.id,
'order_id': order.id
})
trigger_lambda('update-analytics', {
'event': 'flash_sale',
'data': order_data
})
return order
在开发这个多商家商城系统的过程中,最大的教训是要提前设计好数据隔离方案。我们最初采用简单的字段过滤方式,随着商家数量增长出现了严重的性能问题。后来重构为子域名隔离+数据库分片方案后,系统稳定性得到了质的提升。另一个重要经验是小程序端一定要做好图片和数据的懒加载,我们的第一个版本因为一次性加载所有商品数据,导致低端机型频繁崩溃。通过分页加载和图片尺寸优化,用户留存率提高了40%。
