ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Django+微信小程序打造极简健身打卡系统

Django+微信小程序打造极简健身打卡系统

1. 项目背景与核心价值

作为一个长期混迹在健身房的码农,我深知坚持运动有多难。去年用Django+微信小程序给自己做了个健身打卡系统后,不仅体脂降了5个百分点,连代码水平都提高了——因为每天不写完功能就不许健身。这个项目本质上是用技术手段解决人性弱点:通过社交激励+目标拆解+即时反馈的三重机制,把"明天再练"变成"今天必须练"。

市面上现有健身App普遍存在三个痛点:要么功能臃肿(一堆用不上的课程推荐),要么社交属性过重(变成晒照平台),要么数据封闭(无法导出分析)。而我们的解决方案是:

  1. 极简打卡:30秒完成当日记录
  2. 目标可视化:用燃尽图显示计划进度
  3. 数据主权:所有记录可导出为CSV
  4. 轻社交:仅限微信好友间的良性竞争

技术选型上,后端选择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

这个模型设计有几个精妙之处:

  1. 将大目标拆解为每日可执行单元(DailyRecord)
  2. 通过tags字段实现柔性分类(如"晨练"、"HIIT"等)
  3. 采用日期而非datetime存储,避免时区问题

2.3 微信小程序与Django的通信安全

小程序端采用JWT鉴权,特别注意:

  1. 在DRF设置DEFAULT_AUTHENTICATION_CLASSES:
REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': [ 'rest_framework_simplejwt.authentication.JWTAuthentication', ] }
  1. 小程序登录流程:
wx.login({ success: res => { wx.request({ url: '/api/auth/wechat/', method: 'POST', data: { code: res.code } }) } })
  1. 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) # 加入随机性避免枯燥

配合前端实现的"目标协商"界面:

  1. 显示算法推荐值
  2. 允许±2天的调整幅度
  3. 用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点是打卡高峰,我们采用三级缓冲策略:

  1. 前端防抖:提交按钮增加300ms冷却时间
  2. 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"})
  1. 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 排行榜计算优化

社交功能中最耗资源的是好友排行榜,我们采用以下方案:

  1. 预计算:每天凌晨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)])
  1. 客户端缓存:小程序端使用wx.setStorageSync存储上次结果
  2. 增量更新:只返回排名变化超过3位的用户数据

4.3 小程序包体积控制

通过以下手段将包体积控制在1MB以内:

  1. 代码分割:按功能拆分为多个分包
project ├── app.json ├── pages/ │ ├── index/ │ ├── plan/ ├── subpackages/ │ ├── social/ │ ├── stats/
  1. 图片优化:
    • 使用SVG代替PNG图标
    • 重要图片上传CDN
    • 启用微信的图片压缩
  2. 按需引入:
// 而不是一次性导入所有组件 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_appid

5.2 微信小程序审核要点

经历7次审核被拒后总结的避坑清单:

  1. 内容合规:

    • 不得出现"最有效"等绝对化用语
    • BMI计算结果需注明"仅供参考"
    • 用户生成内容必须设置过滤词库
  2. 功能规范:

    • 获取微信运动数据需要明确用途说明
    • 分享功能不能强制要求好友助力
    • 付费功能必须提供测试路径
  3. 技术限制:

    • 不得要求开启蓝牙/定位等无关权限
    • 表单提交必须有防重复机制
    • 页面加载时间超过3秒需添加骨架屏

5.3 监控体系搭建

使用Sentry+Django日志实现三级监控:

  1. 前端错误捕获:
App({ onError(err) { wx.request({ url: '/api/logging/js_error/', method: 'POST', data: { message: err.message, stack: err.stack, version: getApp().version } }) } })
  1. 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 } } }
  1. 关键业务指标监控:
    • 每日活跃用户数(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 }) } }) } }) } })

这个项目给我最大的启示是:技术产品要解决真实痛点,而不是创造需求。最初版本只有简单打卡功能,但通过持续观察用户行为(比如发现很多人会在备注里写训练细节),逐步迭代出了现在的标签系统和社交激励。如果你也在做类似项目,我的建议是:先做减法,再做乘法。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进