1. 项目背景与核心价值
作为一个长期混迹在健身房的码农,我深知坚持运动有多难。去年用Django+微信小程序给自己做了个健身打卡系统后,不仅体脂降了5个百分点,连代码水平都提高了——因为每天不写完功能就不许健身。这个项目本质上是用技术手段解决人性弱点:通过社交激励+目标拆解+即时反馈的三重机制,把"明天再练"变成"今天必须练"。
市面上现有健身App普遍存在三个痛点:要么功能臃肿(一堆用不上的课程推荐),要么社交属性过重(变成晒照平台),要么数据封闭(无法导出分析)。而我们的解决方案是:
- 极简打卡:30秒完成当日记录
- 目标可视化:用燃尽图显示计划进度
- 数据主权:所有记录可导出为CSV
- 轻社交:仅限微信好友间的良性竞争
技术选型上,后端选择Django主要看中其Admin后台的快速开发能力(健身数据需要多维分析),前端用微信小程序则是考虑到:
- 打开即用,无需安装
- 完美利用微信运动数据
- 分享裂变成本极低
关键设计原则:每次训练记录字段不超过5个,但支持自由添加标签。这样既降低用户负担,又保留足够的分析维度。
2. 系统架构设计
2.1 技术栈全景图
微信小程序端 ├── WXML/WXSS 页面布局 ├── JavaScript 业务逻辑 ├── 微信开放能力(运动步数、消息订阅) Django后端服务 ├── RESTful API(DRF) ├── 数据建模(PostgreSQL) ├── 定时任务(Celery) ├── 消息队列(Redis) 基础设施 ├── Nginx 反向代理 ├── Docker 容器化 ├── HTTPS 证书管理2.2 数据库关键模型
class TrainingPlan(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE) title = models.CharField(max_length=100) target_days = models.PositiveIntegerField() start_date = models.DateField() is_public = models.BooleanField(default=False) class DailyRecord(models.Model): plan = models.ForeignKey(TrainingPlan, on_delete=models.CASCADE) date = models.DateField() duration = models.PositiveIntegerField() # 分钟 completed = models.BooleanField(default=False) notes = models.TextField(blank=True) tags = TaggableManager() # django-taggit这个模型设计有几个精妙之处:
- 将大目标拆解为每日可执行单元(DailyRecord)
- 通过tags字段实现柔性分类(如"晨练"、"HIIT"等)
- 采用日期而非datetime存储,避免时区问题
2.3 微信小程序与Django的通信安全
小程序端采用JWT鉴权,特别注意:
- 在DRF设置
DEFAULT_AUTHENTICATION_CLASSES:
REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': [ 'rest_framework_simplejwt.authentication.JWTAuthentication', ] }- 小程序登录流程:
wx.login({ success: res => { wx.request({ url: '/api/auth/wechat/', method: 'POST', data: { code: res.code } }) } })- Django端验证逻辑:
class WeChatAuth(APIView): def post(self, request): code = request.data.get('code') # 通过code向微信服务器换取openid openid = get_openid(code) user, _ = User.objects.get_or_create(wechat_openid=openid) refresh = RefreshToken.for_user(user) return Response({ 'refresh': str(refresh), 'access': str(refresh.access_token), })安全提示:务必在小程序后台配置合法域名,并在Django中启用CORS白名单。我曾因为忘记配置,导致凌晨三点被报警短信吵醒。
3. 核心功能实现细节
3.1 渐进式目标生成算法
很多健身App失败的原因在于目标设定反人性——要么太容易失去挑战性,要么太难导致挫败。我们的解决方案是:
def generate_suggested_plan(current_level): """ 根据用户历史数据推荐新计划 current_level: 过去30天平均训练天数 """ base = max(3, current_level) # 最低3天 upper = min(7, current_level + 2) # 每次增幅不超过2天 return random.randint(base, upper) # 加入随机性避免枯燥配合前端实现的"目标协商"界面:
- 显示算法推荐值
- 允许±2天的调整幅度
- 用emoji表情表示难度等级
3.2 微信运动数据同步
小程序端获取步数权限后,通过加密通道同步到Django:
wx.getWeRunData({ success(res) { const encryptedData = res.encryptedData wx.request({ url: '/api/steps/', method: 'POST', data: { encrypted_data: encryptedData } }) } })后端解密处理:
from Crypto.Cipher import AES def decrypt_wechat_data(encrypted_data, iv, session_key): cipher = AES.new( base64.b64decode(session_key), AES.MODE_CBC, base64.b64decode(iv) ) decrypted = json.loads( cipher.decrypt(base64.b64decode(encrypted_data)) ) return decrypted['stepInfoList']实测发现:微信运动数据有约4小时延迟,所以我们的每日统计截止到前一天24点
3.3 成就系统设计
采用勋章+等级双体系激励:
class Achievement(models.Model): TRIGGER_CHOICES = [ ('consecutive_days', '连续打卡'), ('total_days', '累计天数'), ('total_hours', '累计时长') ] name = models.CharField(max_length=50) trigger_type = models.CharField(max_length=20, choices=TRIGGER_CHOICES) trigger_value = models.PositiveIntegerField() icon = models.ImageField(upload_to='badges/') def check_achievements(user): # 获取用户最新数据 stats = UserStats.objects.get(user=user) # 检查所有可能的成就 unlocked = [] for ach in Achievement.objects.all(): if ach.trigger_type == 'consecutive_days' and stats.consecutive_days >= ach.trigger_value: unlocked.append(ach) # 其他触发条件判断... # 避免重复授予 new_achievements = [a for a in unlocked if not user.achievements.filter(id=a.id).exists()] user.achievements.add(*new_achievements) return new_achievements前端用SVG动画展示成就解锁效果,配合震动反馈:
<wxs module="animation"> function playUnlockAnimation(context) { context.animate('.badge', [ { scale: 0.5, opacity: 0 }, { scale: 1.2, opacity: 1 }, { scale: 1 } ], 1000) } </wxs>4. 性能优化实战
4.1 打卡流量的削峰处理
每天早上7-9点是打卡高峰,我们采用三级缓冲策略:
- 前端防抖:提交按钮增加300ms冷却时间
- Redis缓存:先写入临时记录,后台任务批量入库
def submit_record(request): pipe = redis_client.pipeline() pipe.hset( f"temp_records:{request.user.id}", datetime.today().isoformat(), json.dumps(request.data) ) pipe.expire(f"temp_records:{request.user.id}", 86400) pipe.execute() return Response({"status": "queued"})- Celery定时任务:每10分钟执行一次同步
@app.task def sync_records(): for user_id in redis_client.scan_iter("temp_records:*"): records = redis_client.hgetall(user_id) batch = [DailyRecord(user_id=user_id, **json.loads(v)) for k,v in records.items()] DailyRecord.objects.bulk_create(batch) redis_client.delete(user_id)4.2 排行榜计算优化
社交功能中最耗资源的是好友排行榜,我们采用以下方案:
- 预计算:每天凌晨3点生成静态快照
@app.task def generate_leaderboard(): # 获取所有活跃用户 active_users = User.objects.filter( last_login__gte=timezone.now()-timedelta(days=30) ) # 使用PostgreSQL的窗口函数计算排名 with connection.cursor() as cursor: cursor.execute(""" INSERT INTO leaderboard_snapshot (date, user_id, rank, score) SELECT CURRENT_DATE, user_id, RANK() OVER (ORDER BY SUM(duration) DESC), SUM(duration) FROM fitness_dailyrecord WHERE date >= %s GROUP BY user_id """, [timezone.now()-timedelta(days=7)])- 客户端缓存:小程序端使用
wx.setStorageSync存储上次结果 - 增量更新:只返回排名变化超过3位的用户数据
4.3 小程序包体积控制
通过以下手段将包体积控制在1MB以内:
- 代码分割:按功能拆分为多个分包
project ├── app.json ├── pages/ │ ├── index/ │ ├── plan/ ├── subpackages/ │ ├── social/ │ ├── stats/- 图片优化:
- 使用SVG代替PNG图标
- 重要图片上传CDN
- 启用微信的图片压缩
- 按需引入:
// 而不是一次性导入所有组件 import { Button } from 'tdesign-miniprogram'5. 部署与监控
5.1 Docker化部署
docker-compose.prod.yml关键配置:
services: web: build: . command: gunicorn fitness.wsgi:application --bind 0.0.0.0:8000 volumes: - static:/app/static depends_on: - redis - db db: image: postgres:13 volumes: - pg_data:/var/lib/postgresql/data environment: POSTGRES_PASSWORD: ${DB_PASSWORD} redis: image: redis:6 volumes: - redis_data:/data volumes: pg_data: redis_data: static:启动时通过.env文件注入敏感配置:
DB_PASSWORD=your_strong_password SECRET_KEY=your_django_secret WECHAT_APPID=your_appid5.2 微信小程序审核要点
经历7次审核被拒后总结的避坑清单:
内容合规:
- 不得出现"最有效"等绝对化用语
- BMI计算结果需注明"仅供参考"
- 用户生成内容必须设置过滤词库
功能规范:
- 获取微信运动数据需要明确用途说明
- 分享功能不能强制要求好友助力
- 付费功能必须提供测试路径
技术限制:
- 不得要求开启蓝牙/定位等无关权限
- 表单提交必须有防重复机制
- 页面加载时间超过3秒需添加骨架屏
5.3 监控体系搭建
使用Sentry+Django日志实现三级监控:
- 前端错误捕获:
App({ onError(err) { wx.request({ url: '/api/logging/js_error/', method: 'POST', data: { message: err.message, stack: err.stack, version: getApp().version } }) } })- Django日志配置:
LOGGING = { 'handlers': { 'sentry': { 'level': 'ERROR', 'class': 'sentry_sdk.integrations.logging.EventHandler', }, 'file': { 'level': 'INFO', 'class': 'logging.handlers.TimedRotatingFileHandler', 'filename': '/var/log/django/error.log', 'when': 'midnight', 'backupCount': 7 } } }- 关键业务指标监控:
- 每日活跃用户数(DAU)
- 打卡完成率
- API响应时间P99
6. 项目演进方向
6.1 数据可视化增强
正在开发的热力图组件:
Component({ methods: { renderHeatmap(canvasId, data) { const ctx = wx.createCanvasContext(canvasId) data.forEach((item, index) => { const alpha = item.value / this.data.maxValue ctx.setFillStyle(`rgba(0, 128, 0, ${alpha})`) ctx.fillRect(...this.calcPosition(index)) }) ctx.draw() } } })6.2 智能建议引擎
基于训练记录的推荐算法原型:
from sklearn.neighbors import NearestNeighbors def suggest_workout(user): # 获取所有用户的标准训练数据 all_data = get_normalized_training_data() # 找到相似用户 nn = NearestNeighbors(n_neighbors=5) nn.fit(all_data) distances, indices = nn.kneighbors([user.profile]) # 返回推荐训练组合 return TrainingPlan.objects.filter( user__in=[all_data[i].user for i in indices], rating__gte=4 ).order_by('?')[:3]6.3 硬件设备对接
正在测试的蓝牙设备连接方案:
wx.openBluetoothAdapter({ success: () => { wx.startBluetoothDevicesDiscovery({ services: ['0000fff0-0000-1000-8000-00805f9b34fb'], success: (res) => { wx.onBluetoothDeviceFound((devices) => { if(devices[0].name.includes('FitnessBand')){ wx.createBLEConnection({ deviceId: devices[0].deviceId }) } }) } }) } })这个项目给我最大的启示是:技术产品要解决真实痛点,而不是创造需求。最初版本只有简单打卡功能,但通过持续观察用户行为(比如发现很多人会在备注里写训练细节),逐步迭代出了现在的标签系统和社交激励。如果你也在做类似项目,我的建议是:先做减法,再做乘法。