1. 项目概述
"城市商铺分类信息活动服务平台"是一个典型的O2O(Online To Offline)商业应用,采用前后端分离架构开发。后端使用Python生态的Django或Flask框架构建RESTful API服务,前端通过UniApp实现跨平台移动端应用,最终产物包括微信小程序、Android和iOS应用。平台核心功能围绕城市商业生态展开,主要包括商铺信息展示、分类检索、活动发布、用户互动等模块。
这个项目的独特价值在于将传统分类信息服务与移动互联网深度结合。相比58同城等传统平台,我们更注重本地化、垂直化和实时性,通过小程序即用即走的特性,让用户能快速获取周边商业动态。对于商家而言,平台提供了低成本的数字化营销渠道,特别适合中小型商户开展促销活动。
技术选型上,Django和Flask各有优势:Django自带Admin后台、ORM等全套工具,适合快速构建管理型系统;Flask则更轻量灵活,便于实现定制化接口。UniApp的跨平台能力可以显著降低多端开发成本,一套代码同时覆盖微信、支付宝、百度等小程序平台以及原生App。
2. 技术架构设计
2.1 后端技术栈选型
Django作为全功能框架,内置了用户认证、Admin后台、ORM等组件,适合需要快速开发的管理系统。其MTV模式清晰分离业务逻辑,通过DRF(Django REST Framework)可以快速构建RESTful API。例如商铺模型的序列化器只需几行代码:
class ShopSerializer(serializers.ModelSerializer): class Meta: model = Shop fields = ['id', 'name', 'category', 'address', 'phone']Flask则更适合需要高度定制的场景。使用Flask-RESTful扩展时,API资源类的结构更自由。比如活动接口可以这样定义:
class ActivityApi(Resource): def get(self, shop_id): activities = Activity.query.filter_by(shop_id=shop_id).all() return marshal(activities, activity_fields)数据库方面,MySQL是稳妥选择,PostgreSQL则在GIS地理信息处理上更有优势。对于商铺的地理位置查询,PostGIS扩展支持直接计算经纬度距离:
SELECT * FROM shops WHERE ST_Distance(location, ST_MakePoint(116.404, 39.915)) < 50002.2 前端技术方案
UniApp基于Vue.js生态,使用熟悉的Vue语法即可开发多端应用。其核心优势在于:
- 条件编译:通过特殊注释实现各平台差异化代码
// #ifdef MP-WEIXIN wx.login() // #endif- 原生组件:map、picker等组件在各平台自动适配原生实现
<map :markers="markers" style="width:100%;height:300px"></map>- 插件市场:丰富的第三方插件如uCharts、uView加速开发
对于商铺列表这类高频访问页面,需要特别注意性能优化:
- 使用
<scroll-view>实现上拉加载更多 - 图片懒加载设置
lazy-load属性 - 复杂列表项使用
<recycle-list>组件
2.3 接口设计规范
采用RESTful风格设计API,注意以下要点:
- 资源命名使用复数形式:
GET /api/shops/ POST /api/shops/- 过滤条件通过查询参数传递:
GET /api/shops?category=food&nearby=116.404,39.915- 状态码规范:
- 200 OK - 成功请求
- 201 Created - 资源创建成功
- 400 Bad Request - 参数错误
- 401 Unauthorized - 未认证
- 404 Not Found - 资源不存在
- 数据格式统一:
{ "code": 200, "data": {...}, "message": "success" }3. 核心功能实现
3.1 商铺信息管理
商铺数据模型设计需要考虑以下字段:
class Shop(models.Model): CATEGORY_CHOICES = [ ('food', '餐饮美食'), ('shopping', '购物零售'), ('service', '生活服务') ] name = models.CharField(max_length=100) category = models.CharField(max_length=20, choices=CATEGORY_CHOICES) address = models.TextField() location = models.PointField() # 使用Django-Geo phone = models.CharField(max_length=20) business_hours = models.CharField(max_length=100) description = models.TextField() cover_image = models.ImageField(upload_to='shops/') is_verified = models.BooleanField(default=False)关键接口实现要点:
- 商铺创建需要管理员审核:
@permission_classes([IsAuthenticated]) class ShopCreateAPI(APIView): def post(self, request): serializer = ShopSerializer(data=request.data) if serializer.is_valid(): shop = serializer.save(owner=request.user, is_verified=False) send_verification_email(shop) # 触发审核流程 return Response(serializer.data, status=201) return Response(serializer.errors, status=400)- 地理位置查询使用空间索引:
from django.contrib.gis.measure import D from django.contrib.gis.geos import Point def nearby_shops(request): lat = request.GET.get('lat') lng = request.GET.get('lng') radius = request.GET.get('radius', 5000) # 默认5公里 point = Point(float(lng), float(lat), srid=4326) shops = Shop.objects.filter( location__distance_lte=(point, D(m=radius)) ).annotate( distance=Distance('location', point) ).order_by('distance') serializer = ShopSerializer(shops, many=True) return Response(serializer.data)3.2 活动发布系统
活动模型与商铺关联设计:
class Activity(models.Model): shop = models.ForeignKey(Shop, on_delete=models.CASCADE) title = models.CharField(max_length=100) content = models.TextField() start_time = models.DateTimeField() end_time = models.DateTimeField() cover_image = models.ImageField(upload_to='activities/') is_featured = models.BooleanField(default=False) class Meta: ordering = ['-start_time']活动状态需要实时计算:
@property def status(self): now = timezone.now() if now < self.start_time: return 'upcoming' elif self.start_time <= now <= self.end_time: return 'ongoing' else: return 'ended'前端活动卡片组件示例:
<template> <view class="activity-card" @click="navigateToDetail"> <image :src="activity.cover_image" mode="aspectFill"></image> <view class="badge" :class="activity.status"> {{ activity.status | statusText }} </view> <view class="info"> <text class="title">{{ activity.title }}</text> <text class="shop">{{ activity.shop.name }}</text> <text class="time">{{ activity.timeRange }}</text> </view> </view> </template> <script> export default { filters: { statusText(status) { const map = { upcoming: '未开始', ongoing: '进行中', ended: '已结束' } return map[status] || '' } } } </script>3.3 用户交互设计
收藏功能的实现方案:
- 使用中间表记录用户-商铺关系
class Favorite(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE) shop = models.ForeignKey(Shop, on_delete=models.CASCADE) created_at = models.DateTimeField(auto_now_add=True) class Meta: unique_together = ('user', 'shop')- 接口处理幂等性:
@action(detail=True, methods=['post']) def favorite(self, request, pk=None): shop = self.get_object() fav, created = Favorite.objects.get_or_create( user=request.user, shop=shop ) if not created: fav.delete() return Response({'status': 'removed'}) return Response({'status': 'added'})- 前端交互优化:
async toggleFavorite() { try { const res = await this.$http.post(`/api/shops/${this.shop.id}/favorite/`) this.isFavorited = res.data.status === 'added' uni.showToast({ title: this.isFavorited ? '收藏成功' : '已取消收藏', icon: 'none' }) } catch (e) { console.error(e) } }4. 性能优化实践
4.1 数据库查询优化
- 使用select_related/prefetch_related减少查询次数:
# 错误做法:N+1查询问题 shops = Shop.objects.all() for shop in shops: print(shop.owner.username) # 每次循环都查询user表 # 正确做法 shops = Shop.objects.select_related('owner').all()- 添加适当索引:
class Activity(models.Model): shop = models.ForeignKey(Shop, on_delete=models.CASCADE, db_index=True) start_time = models.DateTimeField(db_index=True) end_time = models.DateTimeField(db_index=True)- 分页查询实现:
class ShopListView(ListAPIView): queryset = Shop.objects.filter(is_verified=True) serializer_class = ShopSerializer pagination_class = PageNumberPagination def get_queryset(self): queryset = super().get_queryset() category = self.request.query_params.get('category') if category: queryset = queryset.filter(category=category) return queryset4.2 缓存策略设计
- 使用Redis缓存热门数据:
from django.core.cache import cache def get_featured_shops(): cache_key = 'featured_shops' shops = cache.get(cache_key) if not shops: shops = list(Shop.objects.filter(is_featured=True)[:10]) cache.set(cache_key, shops, timeout=3600) # 缓存1小时 return shops- 接口响应缓存装饰器:
from django.views.decorators.cache import cache_page @cache_page(60 * 15) # 缓存15分钟 def shop_list(request): # ...- 前端数据缓存策略:
// 使用uniapp的storage API const getCachedShops = async () => { try { const cached = uni.getStorageSync('cachedShops') if (cached && Date.now() - cached.timestamp < 3600000) { return cached.data } const res = await this.$http.get('/api/shops/') uni.setStorageSync('cachedShops', { data: res.data, timestamp: Date.now() }) return res.data } catch (e) { console.error(e) return [] } }4.3 图片处理优化
- 使用七牛云等CDN加速:
# settings.py DEFAULT_FILE_STORAGE = 'qiniustorage.backends.QiniuStorage' QINIU_ACCESS_KEY = 'your_access_key' QINIU_SECRET_KEY = 'your_secret_key' QINIU_BUCKET_NAME = 'your_bucket_name' QINIU_BUCKET_DOMAIN = 'cdn.yourdomain.com'- 前端图片懒加载:
<image :src="shop.cover_image" mode="aspectFill" lazy-load :fade-show="false" ></image>- 缩略图生成策略:
from django_resized import ResizedImageField class Shop(models.Model): cover_image = ResizedImageField( size=[800, 600], quality=85, upload_to='shops/' )5. 部署与运维方案
5.1 后端部署方案
Django推荐部署架构:
Nginx ←→ Gunicorn ←→ Django关键配置示例:
# gunicorn.conf.py workers = 3 worker_class = 'gevent' bind = '0.0.0.0:8000' timeout = 120Nginx配置要点:
location / { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location /static/ { alias /path/to/static/files/; expires 30d; }5.2 前端多端发布
UniApp发布流程:
- 微信小程序:
npm run build:mp-weixin然后在微信开发者工具中上传
- Android App打包:
npm run build:app-plus生成apk文件
- 发布H5版本:
npm run build:h5部署到Nginx或对象存储
5.3 监控与日志
- 使用Sentry捕获错误:
# settings.py import sentry_sdk sentry_sdk.init( dsn="your_dsn", traces_sample_rate=1.0 )- 日志配置:
LOGGING = { 'handlers': { 'file': { 'level': 'INFO', 'class': 'logging.FileHandler', 'filename': '/var/log/django.log', }, }, 'loggers': { 'django': { 'handlers': ['file'], 'level': 'INFO', }, }, }- 性能监控:
# 安装Prometheus客户端 pip install django-prometheus # settings.py INSTALLED_APPS += ['django_prometheus'] MIDDLEWARE.insert(0, 'django_prometheus.middleware.PrometheusBeforeMiddleware')6. 常见问题与解决方案
6.1 跨域问题处理
Django跨域配置:
# settings.py INSTALLED_APPS += ['corsheaders'] MIDDLEWARE.insert(0, 'corsheaders.middleware.CorsMiddleware') CORS_ALLOWED_ORIGINS = [ 'https://yourdomain.com', 'https://app.yourdomain.com' ]Flask跨域处理:
from flask_cors import CORS app = Flask(__name__) CORS(app, resources={ r"/api/*": {"origins": "*"} })6.2 微信登录集成
- 获取code:
uni.login({ provider: 'weixin', success: (res) => { this.code = res.code } })- 后端验证流程:
def wechat_login(request): code = request.data.get('code') appid = settings.WECHAT_APPID secret = settings.WECHAT_SECRET # 获取openid resp = requests.get( 'https://api.weixin.qq.com/sns/jscode2session', params={ 'appid': appid, 'secret': secret, 'js_code': code, 'grant_type': 'authorization_code' } ) data = resp.json() openid = data.get('openid') # 查找或创建用户 user, created = User.objects.get_or_create( wechat_openid=openid, defaults={'username': f'wx_{openid[:8]}'} ) # 生成JWT token token = generate_jwt_token(user) return Response({'token': token})6.3 地图集成问题
UniApp中使用腾讯地图:
<map id="map" :latitude="latitude" :longitude="longitude" :markers="markers" @markertap="handleMarkerTap" ></map>坐标转换问题处理:
// 将GCJ-02(腾讯地图)转为WGS84(GPS标准) function gcj02towgs84(lng, lat) { const ee = 0.006693421622965943 const a = 6378245.0 if (outOfChina(lng, lat)) { return [lng, lat] } let dlat = transformlat(lng - 105.0, lat - 35.0) let dlng = transformlng(lng - 105.0, lat - 35.0) const radlat = lat / 180.0 * Math.PI let magic = Math.sin(radlat) magic = 1 - ee * magic * magic const sqrtmagic = Math.sqrt(magic) dlat = (dlat * 180.0) / ((a * (1 - ee)) / (magic * sqrtmagic) * Math.PI) dlng = (dlng * 180.0) / (a / sqrtmagic * Math.cos(radlat) * Math.PI) return [lng - dlng, lat - dlat] }7. 项目扩展方向
7.1 智能推荐系统
基于用户行为的协同过滤:
from surprise import Dataset, KNNBasic def recommend_shops(user_id): # 加载用户-商铺交互数据 data = Dataset.load_from_df(interactions_df, reader) trainset = data.build_full_trainset() # 使用KNN算法 algo = KNNBasic() algo.fit(trainset) # 获取推荐 inner_uid = trainset.to_inner_uid(user_id) neighbors = algo.get_neighbors(inner_uid, k=5) return Shop.objects.filter( id__in=[trainset.to_raw_iid(i) for i in neighbors] )7.2 即时通讯功能
集成WebSocket实现聊天:
# consumers.py class ChatConsumer(AsyncWebsocketConsumer): async def connect(self): self.room_name = self.scope['url_route']['kwargs']['room_name'] self.room_group_name = f'chat_{self.room_name}' await self.channel_layer.group_add( self.room_group_name, self.channel_name ) await self.accept() async def receive(self, text_data): await self.channel_layer.group_send( self.room_group_name, { 'type': 'chat_message', 'message': text_data } ) async def chat_message(self, event): await self.send(text_data=event['message'])7.3 数据分析看板
使用Django-admin定制数据分析:
@admin.register(Shop) class ShopAdmin(admin.ModelAdmin): change_list_template = 'admin/shop_changelist.html' def changelist_view(self, request, extra_context=None): response = super().changelist_view(request, extra_context) # 添加统计数据 stats = { 'total_shops': Shop.objects.count(), 'active_shops': Shop.objects.filter(is_verified=True).count(), 'top_categories': Shop.objects.values('category') .annotate(count=Count('id')) .order_by('-count')[:5] } if hasattr(response, 'context_data'): response.context_data['stats'] = stats return response前端使用uCharts展示:
<template> <view> <qiun-data-charts type="pie" :chartData="chartData" /> </view> </template> <script> export default { data() { return { chartData: { series: [{ data: [ {name: '餐饮', value: 45}, {name: '零售', value: 30}, {name: '服务', value: 25} ] }] } } } } </script>8. 开发经验与避坑指南
8.1 微信小程序特有问题
- 图片域名白名单:
- 需要在微信公众平台配置合法域名
- 开发阶段可勾选"不校验合法域名"
- 用户授权策略变更:
- 必须使用button组件触发授权
<button open-type="getUserInfo" @getuserinfo="getUserInfo">授权登录</button>- 小程序包大小限制:
- 主包不超过2MB
- 使用分包加载技术:
{ "subPackages": [{ "root": "packageA", "pages": [ "pages/shop/list", "pages/shop/detail" ] }] }8.2 UniApp常见陷阱
- 条件编译问题:
// 错误写法:条件编译注释必须独占一行 const isWeapp = // #ifdef MP-WEIXIN true // #endif // 正确写法 // #ifdef MP-WEIXIN const isWeapp = true // #endif- 样式兼容性:
- 使用flex布局时添加前缀:
.flex { display: -webkit-flex; display: flex; }- 原生组件层级问题:
- map、video等原生组件总是最高层级
- 使用cover-view覆盖原生组件
8.3 Django/Flask性能陷阱
- N+1查询问题:
# 错误做法 for shop in Shop.objects.all(): print(shop.owner.username) # 每次循环查询user表 # 正确做法 for shop in Shop.objects.select_related('owner').all(): print(shop.owner.username)- 分页性能优化:
# 使用values()只取必要字段 Shop.objects.filter(category='food').values('id', 'name', 'address')[:10] # 大数据量分页使用游标分页 from rest_framework.pagination import CursorPagination- 信号处理器性能:
# 避免在信号中执行耗时操作 @receiver(post_save, sender=Shop) def update_shop_count(sender, instance, **kwargs): # 错误:直接更新统计 Stats.objects.update(shop_count=Shop.objects.count()) # 正确:使用异步任务 update_shop_count_task.delay()9. 测试策略与质量保障
9.1 单元测试设计
Django测试示例:
class ShopTestCase(TestCase): def setUp(self): self.user = User.objects.create(username='test') self.shop_data = { 'name': '测试店铺', 'category': 'food', 'address': '测试地址' } def test_shop_creation(self): shop = Shop.objects.create(owner=self.user, **self.shop_data) self.assertEqual(shop.is_verified, False) self.assertEqual(shop.owner.username, 'test') def test_shop_str(self): shop = Shop.objects.create(owner=self.user, **self.shop_data) self.assertEqual(str(shop), '测试店铺')Flask测试方案:
def test_activity_api(client): # 创建测试数据 shop = Shop(name='测试店铺') db.session.add(shop) db.session.commit() # 测试接口 resp = client.get(f'/api/shops/{shop.id}/activities/') assert resp.status_code == 200 assert len(resp.json) == 09.2 接口自动化测试
使用Postman Collection:
- 创建测试集合
- 添加环境变量(base_url, token等)
- 编写测试脚本:
pm.test("Status code is 200", function() { pm.response.to.have.status(200); }); pm.test("Response has data field", function() { const jsonData = pm.response.json(); pm.expect(jsonData).to.have.property('data'); });- 配置CI/CD自动运行:
# .github/workflows/api-test.yml jobs: test: steps: - uses: actions/checkout@v2 - uses: actions/setup-node@v2 - run: npm install -g newman - run: newman run collection.json -e environment.json9.3 前端E2E测试
UniApp使用uni-automator:
- 安装测试工具:
npm install @dcloudio/uni-automator --save-dev- 编写测试用例:
describe('商铺列表', () => { it('应显示商铺列表', async () => { await page.goto('/pages/shop/list') await page.waitFor(1000) const items = await page.$$('.shop-item') expect(items.length).toBeGreaterThan(0) }) })- 集成到HBuilderX:
- 创建测试配置文件
- 配置运行脚本
- 查看测试报告
10. 项目演进与迭代
10.1 第一阶段:核心功能MVP
- 时间规划:4-6周
- 核心目标:
- 商铺信息管理(CRUD)
- 基础分类检索
- 用户收藏功能
- 微信小程序端实现
- 交付标准:
- 后台管理系统可审核商铺
- 小程序能展示附近商铺
- 完成基础接口测试
10.2 第二阶段:功能增强
- 时间规划:2-3周
- 新增功能:
- 活动发布系统
- 用户评价功能
- 数据统计看板
- Android/iOS应用发布
- 技术重点:
- 优化地理位置查询
- 实现活动状态机
- 增强后台管理功能
10.3 第三阶段:生态扩展
- 时间规划:持续迭代
- 扩展方向:
- 商家端管理后台
- 会员积分系统
- 智能推荐引擎
- 即时通讯功能
- 数据分析平台
- 技术演进:
- 引入微服务架构
- 增加消息队列
- 实现大数据分析
- 构建推荐系统
在实际开发中,我们采用了敏捷开发模式,每两周一个迭代周期。每个迭代都包含需求评审、任务拆分、开发实现、测试验证和演示回顾五个环节。这种模式特别适合此类需求可能频繁变化的商业应用项目。