ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Django与UniApp构建O2O商铺平台的技术实践

2026/9/16 18:29:59 拓冰建站 浏览量
Django与UniApp构建O2O商铺平台的技术实践 1. 项目概述城市商铺分类信息活动服务平台是一个典型的O2OOnline 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模式清晰分离业务逻辑通过DRFDjango 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_idshop_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 :markersmarkers stylewidth: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?categoryfoodnearby116.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_length100) category models.CharField(max_length20, choicesCATEGORY_CHOICES) address models.TextField() location models.PointField() # 使用Django-Geo phone models.CharField(max_length20) business_hours models.CharField(max_length100) description models.TextField() cover_image models.ImageField(upload_toshops/) is_verified models.BooleanField(defaultFalse)关键接口实现要点商铺创建需要管理员审核permission_classes([IsAuthenticated]) class ShopCreateAPI(APIView): def post(self, request): serializer ShopSerializer(datarequest.data) if serializer.is_valid(): shop serializer.save(ownerrequest.user, is_verifiedFalse) send_verification_email(shop) # 触发审核流程 return Response(serializer.data, status201) return Response(serializer.errors, status400)地理位置查询使用空间索引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), srid4326) shops Shop.objects.filter( location__distance_lte(point, D(mradius)) ).annotate( distanceDistance(location, point) ).order_by(distance) serializer ShopSerializer(shops, manyTrue) return Response(serializer.data)3.2 活动发布系统活动模型与商铺关联设计class Activity(models.Model): shop models.ForeignKey(Shop, on_deletemodels.CASCADE) title models.CharField(max_length100) content models.TextField() start_time models.DateTimeField() end_time models.DateTimeField() cover_image models.ImageField(upload_toactivities/) is_featured models.BooleanField(defaultFalse) 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 classactivity-card clicknavigateToDetail image :srcactivity.cover_image modeaspectFill/image view classbadge :classactivity.status {{ activity.status | statusText }} /view view classinfo text classtitle{{ activity.title }}/text text classshop{{ activity.shop.name }}/text text classtime{{ activity.timeRange }}/text /view /view /template script export default { filters: { statusText(status) { const map { upcoming: 未开始, ongoing: 进行中, ended: 已结束 } return map[status] || } } } /script3.3 用户交互设计收藏功能的实现方案使用中间表记录用户-商铺关系class Favorite(models.Model): user models.ForeignKey(User, on_deletemodels.CASCADE) shop models.ForeignKey(Shop, on_deletemodels.CASCADE) created_at models.DateTimeField(auto_now_addTrue) class Meta: unique_together (user, shop)接口处理幂等性action(detailTrue, methods[post]) def favorite(self, request, pkNone): shop self.get_object() fav, created Favorite.objects.get_or_create( userrequest.user, shopshop ) 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减少查询次数# 错误做法N1查询问题 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_deletemodels.CASCADE, db_indexTrue) start_time models.DateTimeField(db_indexTrue) end_time models.DateTimeField(db_indexTrue)分页查询实现class ShopListView(ListAPIView): queryset Shop.objects.filter(is_verifiedTrue) 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(categorycategory) 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_featuredTrue)[:10]) cache.set(cache_key, shops, timeout3600) # 缓存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 :srcshop.cover_image modeaspectFill lazy-load :fade-showfalse /image缩略图生成策略from django_resized import ResizedImageField class Shop(models.Model): cover_image ResizedImageField( size[800, 600], quality85, upload_toshops/ )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( dsnyour_dsn, traces_sample_rate1.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 微信登录集成获取codeuni.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_openidopenid, defaults{username: fwx_{openid[:8]}} ) # 生成JWT token token generate_jwt_token(user) return Response({token: token})6.3 地图集成问题UniApp中使用腾讯地图map idmap :latitudelatitude :longitudelongitude :markersmarkers markertaphandleMarkerTap /map坐标转换问题处理// 将GCJ-02腾讯地图转为WGS84GPS标准 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, k5) 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 fchat_{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_dataevent[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_contextNone): response super().changelist_view(request, extra_context) # 添加统计数据 stats { total_shops: Shop.objects.count(), active_shops: Shop.objects.filter(is_verifiedTrue).count(), top_categories: Shop.objects.values(category) .annotate(countCount(id)) .order_by(-count)[:5] } if hasattr(response, context_data): response.context_data[stats] stats return response前端使用uCharts展示template view qiun-data-charts typepie :chartDatachartData / /view /template script export default { data() { return { chartData: { series: [{ data: [ {name: 餐饮, value: 45}, {name: 零售, value: 30}, {name: 服务, value: 25} ] }] } } } } /script8. 开发经验与避坑指南8.1 微信小程序特有问题图片域名白名单需要在微信公众平台配置合法域名开发阶段可勾选不校验合法域名用户授权策略变更必须使用button组件触发授权button open-typegetUserInfo getuserinfogetUserInfo授权登录/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性能陷阱N1查询问题# 错误做法 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(categoryfood).values(id, name, address)[:10] # 大数据量分页使用游标分页 from rest_framework.pagination import CursorPagination信号处理器性能# 避免在信号中执行耗时操作 receiver(post_save, senderShop) def update_shop_count(sender, instance, **kwargs): # 错误直接更新统计 Stats.objects.update(shop_countShop.objects.count()) # 正确使用异步任务 update_shop_count_task.delay()9. 测试策略与质量保障9.1 单元测试设计Django测试示例class ShopTestCase(TestCase): def setUp(self): self.user User.objects.create(usernametest) self.shop_data { name: 测试店铺, category: food, address: 测试地址 } def test_shop_creation(self): shop Shop.objects.create(ownerself.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(ownerself.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/checkoutv2 - uses: actions/setup-nodev2 - 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 第三阶段生态扩展时间规划持续迭代扩展方向商家端管理后台会员积分系统智能推荐引擎即时通讯功能数据分析平台技术演进引入微服务架构增加消息队列实现大数据分析构建推荐系统在实际开发中我们采用了敏捷开发模式每两周一个迭代周期。每个迭代都包含需求评审、任务拆分、开发实现、测试验证和演示回顾五个环节。这种模式特别适合此类需求可能频繁变化的商业应用项目。