
1. 这个项目到底解决什么问题从题目拆解到落地场景“django基于Python的学生移动端数据分析小程序设计与实现”这个标题拆开看就四个关键词django、Python、移动端、数据分析。我第一次看到这个题目的时候第一反应是“这又是一个课程设计级别的全栈项目”但真正把它做下来之后发现这里面的坑和细节远比标题本身看起来要多。它本质上是一个典型的“后端重逻辑、前端轻展示”的小程序项目后端负责数据采集、清洗、聚合分析小程序端负责把分析结果用图表和列表的方式呈现给终端用户。先聊聊为什么这个技术组合在实际场景里非常合理。学生移动端数据分析核心场景通常分两类一类是校园管理方需要看学生行为数据比如一卡通消费记录、图书馆入馆频次、体测成绩变化、课程考勤统计另一类是学生自己需要查看个人数据报告比如月度消费构成、运动步数趋势、学习时长分布。不管哪一类数据源都是结构化数据分析逻辑集中在后端前端只需要做“查询—展示”。django在这个场景里的优势非常明显。它自带的ORM可以让你用Python代码操作数据库不用写一行原生SQL就能完成分组、求和、均值、排序这些统计操作配合annotate和aggregate这两个方法数据分析接口的代码量能压缩到非常短。而且django自带的admin后台可以零成本做数据管理开发阶段直接把模拟数据灌进去用admin就能看到分析结果这在纯后端调试阶段特别省事。小程序端选择原生微信小程序而不是uniapp是我个人在这个项目里的一个倾向性选择。原因很简单这个项目的页面复杂度并不高主要就是首页的数据概览、分析报告页、个人中心三个页面原生小程序完全够用而且原生小程序的图表组件ec-canvas坑更少调试工具也更成熟。如果页面再多一点、需要多端复用那时候再上uniapp不迟但在这个项目体量里引入跨端框架反而增加了一层构建复杂度。这个项目适合谁来参考我觉得有三类人第一类是正在做课程设计或毕业设计的计算机相关专业学生这个题目本身就是个典型的毕设选题第二类是刚接触django、想找一个“不是博客也不是商城”的实战项目的开发者因为数据分析场景能让你真正用上ORM的聚合功能而不是只会写增删改查第三类是校园信息化相关的工作人员想快速搭一个学生数据可视化查询工具做内部试点。下面我把整个项目的实现过程拆开讲从后端到前端从接口设计到图表渲染全部是基于我实际跑通过的方案。2. 后端设计django如何撑起数据分析的数据底座2.1 数据模型设计先想清楚要分析什么很多新手做数据分析类项目上来就写models结果写到一半发现字段设计不合理统计接口要么写不出来要么效率极低。我的建议是先想清楚你要分析什么指标再倒推表结构。我做的版本选择了一个非常典型且数据易得的场景——校园一卡通消费数据分析。这个场景好在数据天然结构化每条消费记录包含学生、商户、金额、时间四个核心维度做出来的分析报告也贴近真实需求月度总消费、消费结构食堂/超市/浴室、餐均消费、消费时段分布、异常消费提醒。对应到django的模型我设计了三个核心表# models.py 核心模型设计 from django.db import models from django.contrib.auth.models import User class Student(models.Model): 学生信息表 user models.OneToOneField(User, on_deletemodels.CASCADE, related_namestudent) student_no models.CharField(学号, max_length20, uniqueTrue) name models.CharField(姓名, max_length50) college models.CharField(学院, max_length100) grade models.CharField(年级, max_length20) # 如 2023级 balance models.DecimalField(卡内余额, max_digits8, decimal_places2, default0) class Meta: db_table student verbose_name 学生信息 verbose_name_plural 学生信息 def __str__(self): return f{self.student_no} - {self.name} class Merchant(models.Model): 商户表食堂窗口/超市/浴室等 name models.CharField(商户名称, max_length100) category models.CharField(商户类别, max_length20) # 餐饮、超市、洗浴、其他 location models.CharField(位置, max_length100, blankTrue) class Meta: db_table merchant def __str__(self): return self.name class ConsumptionRecord(models.Model): 一卡通消费记录表 student models.ForeignKey(Student, on_deletemodels.CASCADE, related_nameconsumptions) merchant models.ForeignKey(Merchant, on_deletemodels.CASCADE, related_namerecords) amount models.DecimalField(消费金额, max_digits6, decimal_places2) consumed_at models.DateTimeField(消费时间, db_indexTrue) class Meta: db_table consumption_record verbose_name 消费记录 verbose_name_plural 消费记录 ordering [-consumed_at] def __str__(self): return f{self.student.student_no} 于 {self.consumed_at} 消费 {self.amount}元这个设计的关键点在于ConsumptionRecord表上设置了consumed_at的db_index索引实际做时间范围聚合查询时这个索引能直接决定接口响应速度——不加索引10万条记录的按日期分组可能要1秒多加了索引能压到200毫秒以内。另外学生和消费记录之间用了ForeignKey并设置了related_name后面写分析接口时直接通过student.consumptions.all()就能访问该学生的全部消费记录语义清晰不需要额外拼接查询条件。还要说明一下为什么用django自带的User表做一对一关联而不是自己另建一张登录表。因为小程序端的登录态最终还是要靠openid来换token后端用户体系复用django自带User是最稳妥的方案权限控制、session管理都是现成的不需要自己造轮子。2.2 数据分析的核心逻辑用ORM聚合函数替代手写SQL数据模型建好之后真正的重头戏是分析接口的编写。django的ORM提供了两个关键方法aggregate和annotate。初学者最容易搞混这两个方法的区别。简单说aggregate返回一个汇总后的字典比如总金额、平均值、最大最小值适合算总账annotate是在每个分组对象上挂载一个统计值返回一个QuerySet适合做分组统计。数据分析场景里annotate用的频次远高于aggregate因为绝大多数报表都是“按XX维度分组看统计值”。比如我要实现“近30天每日消费总额趋势”核心逻辑就三行from django.db.models.functions import TruncDate from django.db.models import Sum daily_data ( ConsumptionRecord.objects .filter( studentrequest.user.student, consumed_at__gtetimezone.now() - timedelta(days30) ) .annotate(dayTruncDate(consumed_at)) .values(day) .annotate(totalSum(amount)) .order_by(day) )这里面的关键点是TruncDate函数它的作用是把datetime类型截断成date类型这样按天分组就非常干净。如果不截断直接用consumed_at分组因为每条记录的时间戳都不一样实际上根本不会产生“分组”效果每条记录都会单独占一组这是新手最容易踩的坑。再比如“月度消费结构分析”需要按商户类别汇总category_stats ( ConsumptionRecord.objects .filter(studentstu, consumed_at__yearyear, consumed_at__monthmonth) .values(merchant__category) .annotate( totalSum(amount), countCount(id) ) .order_by(-total) )注意values(merchant__category)这种写法它通过外键字段直接跨表取商户类别django会把这一句翻译成带JOIN的SQL你不需要显式写任何JOIN语句。返回的结果是一个包含字典的QuerySet比如[{merchant__category: 餐饮, total: Decimal(523.50), count: 36}, ...]直接JSON序列化就能给小程序用。实操中我还发现一个性能细节如果分析接口要同时算很多个指标尽量一次性把需要的数据查出来在Python内存里做二次计算而不是对数据库反复发起多次查询。比如计算“月度总消费消费笔数餐均消费”这组指标用一次聚合比三次独立查询快得多from django.db.models import Avg, Count, Sum stats ( ConsumptionRecord.objects .filter(studentstu, consumed_at__yeary, consumed_at__monthm) .aggregate( total_spentSum(amount), total_countCount(id), avg_per_mealAvg(amount) ) )一次请求拿到三个指标数据库IO少了两轮小程序端也只需要等一个接口。2.3 API接口设计RESTful风格还是轻量JSON接口数据分析小程序的接口设计我不建议在这个量级的项目里引入完整的DRFDjango REST Framework尽管它是django生态里最流行的API框架。原因是这个项目的接口数量通常在10个以内鉴权走的是自定义token序列化对象也就是学生信息和统计数据DRF的Serializer、ViewSet、Permission体系在这里发挥不出太大优势反而增加了学习成本和代码量。我采用的是轻量方案基于django原生JsonResponse 自定义装饰器做登录校验 简单的URL路由。核心接口就这么几个接口方法功能返回数据/api/auth/login/POST小程序登录code换openidtoken、学生基本信息/api/report/overview/GET首页概览余额、本月累计消费、消费笔数、日均消费统计值趋势数据/api/report/category/GET消费结构分析按商户类别汇总类别名、金额、占比/api/report/trend/GET按月/按周消费趋势时间序列金额序列/api/report/detail/GET消费明细列表分页的消费记录写一个接口的大致模板是import json from django.http import JsonResponse from django.views.decorators.http import require_http_methods def api_response(code0, dataNone, msgsuccess): 统一响应格式 return JsonResponse({code: code, data: data, msg: msg}) def login_required_custom(func): 自定义登录校验装饰器 def wrapper(request, *args, **kwargs): token request.META.get(HTTP_AUTHORIZATION, ) student TokenManager.get_student_by_token(token) if not student: return api_response(code401, msg登录状态已失效) request.student student return func(request, *args, **kwargs) return wrapper require_http_methods([GET]) login_required_custom def overview(request): student request.student # 调数据分析函数... return api_response(datareport_data)统一把返回格式固定为{code, data, msg}三层结构小程序端只需要无脑检查code是否为0不用每次去解析不同的字段结构。这个习惯看起来很小实际联调的时候能省非常多时间。接口报错了msg字段直接把错误原因带出去排查问题一眼定位。2.4 后端数据灌入与模拟数据生成数据分析项目最怕的是“没数据可分析”。开发阶段不可能拿真实一卡通数据来调试所以必须有一套模拟数据生成脚本。这个脚本我有两个使用原则第一数据量要够大至少几万条否则聚合分析跑不出效果第二数据分布要模拟真实场景比如早餐少吃、晚餐多吃、月底消费比月初低、周末浴室消费高这样才能看出分析图表的意义。我用django的management command机制写了一个灌数命令直接python manage.py generate_data就能跑# management/commands/generate_data.py import random from datetime import timedelta from django.core.management.base import BaseCommand from django.utils import timezone from app.models import Student, Merchant, ConsumptionRecord class Command(BaseCommand): help 生成模拟消费数据 def handle(self, *args, **options): # 先清理旧数据保证可重复执行 ConsumptionRecord.objects.all().delete() # 创建示例学生 students [] for i in range(50): stu Student.objects.create( student_nof2023{i:04d}, namef学生{i:02d}, college信息工程学院, gradef{2020 i % 4}级 ) students.append(stu) # 创建商户 merchants { 第一食堂一楼: 餐饮, 第二食堂二楼: 餐饮, 校园超市: 超市, 公共浴室: 洗浴 } merchant_objs [] for name, cat in merchants.items(): merchant_objs.append(Merchant.objects.create(namename, categorycat)) # 生成一年内的消费记录 now timezone.now() for i in range(80000): stu random.choice(students) merchant random.choice(merchant_objs) # 不同消费时段金额分布不同模拟真实消费 if merchant.category 餐饮: amount round(random.uniform(3.5, 28), 2) elif merchant.category 超市: amount round(random.uniform(1, 99), 2) else: amount round(random.uniform(1, 8), 2) delta_days random.randint(0, 365) delta_hour random.randint(6, 22) consumed_at now - timedelta(daysdelta_days, hoursnow.hour - delta_hour, minutesrandom.randint(0, 59)) ConsumptionRecord.objects.create( studentstu, merchantmerchant, amountamount, consumed_atconsumed_at ) self.stdout.write(self.style.SUCCESS(f成功生成80000条消费记录))注意代码里我用random.uniform控制金额范围用random.randint(0, 365)控制时间跨度这样生成的消费记录在一年内均匀分布分析图表画出来就有“近一年每月消费趋势”的效果。自己写灌数脚本的时候尽量把时间、金额分布做得有规律一些否则图表画出来是一团噪声看不出任何分析价值。3. 小程序端实现从登录态到图表展示3.1 微信登录与后端token的无缝对接小程序端第一关就是登录。微信小程序的登录流程说简单也简单说复杂也复杂。核心链路是小程序端调用wx.login()拿到一个临时code把code发给后端后端拿code加上小程序的AppID和AppSecret请求微信的code2Session接口换取openid和session_key后端再根据openid找到或创建对应的学生账号签发一个自己的token给小程序。这里有一个无数人踩过的坑token不能直接加密openid给前端而是必须服务端保存一个随机字符串token并建立token到openid的映射。因为小程序端存storage的东西理论上都是可以被读取的如果直接把openid当token用等于把用户身份明文暴露了。我实际项目里的做法是生成一个secrets.token_hex(16)的随机串存在一张AuthToken表里带上过期时间请求进入时先查token表定位到user再取student。# 登录接口核心逻辑 import requests # 注意是requests不是urllibpython3里requests更顺手 from django.conf import settings from django.utils import timezone from datetime import timedelta import secrets from .models import AuthToken, Student def wx_login(code): # 向微信服务器换取openid url https://api.weixin.qq.com/sns/jscode2session params { appid: settings.WX_APPID, secret: settings.WX_SECRET, js_code: code, grant_type: authorization_code } resp requests.get(url, paramsparams, timeout5).json() openid resp.get(openid) if not openid: return None, 微信登录失败 # 查找或创建学生绑定到django User user, _ User.objects.get_or_create( usernamefwx_{openid}, defaults{password: } ) student, _ Student.objects.get_or_create( useruser, defaults{ student_no: fTEMP{openid[-4:]}, name: 新用户 } ) # 签发自己的token token secrets.token_hex(16) AuthToken.objects.create( studentstudent, tokentoken, expires_attimezone.now() timedelta(days7) ) return token, None这里要特别提醒一个细节requests.get调用微信接口时一定要设置timeout不设的话一旦微信接口无响应你的django服务会一直挂起小程序端表现为“请求一直转圈”。另外get_or_create是新用户自动注册的关键不需要单独的注册接口第一次登录自动建档这个体验对小程序来说非常重要。小程序端登录的代码也非常简短// app.js 登录逻辑 App({ onLaunch() { this.login() }, login() { wx.login({ success: async (res) { const { code } res const loginRes await new Promise((resolve, reject) { wx.request({ url: http://你的域名/api/auth/login/, method: POST, data: { code }, success: resolve, fail: reject }) }) if (loginRes.data.code 0) { wx.setStorageSync(token, loginRes.data.data.token) wx.setStorageSync(studentInfo, loginRes.data.data.student) } } }) } })开发阶段还有一个必须知道的事小程序必须在开发者工具里勾选“不校验合法域名”才能访问http://127.0.0.1:8000这类本地地址。上线就换成正式的HTTPS域名并且在微信公众平台里配置request合法域名否则真机预览直接白屏请求全部被拦。这个坑几乎每个新手都会踩我记得第一次真机调试的时候忘了配域名排查了整整一下午。3.2 页面架构与数据分析图表的渲染小程序端我规划了三个tab页首页数据概览、分析报告、个人中心。首页放核心指标卡片和趋势折线图分析报告页放消费结构饼图和明细列表个人中心放学生信息、余额、退出登录。前端页面用原生语法写wxmlwxssjs三件套。需要强调的一点是小程序不支持直接渲染HTML也不支持DOM操作所有界面更新必须通过setData完成。所以页面里凡是需要动态展示的数据都要在data里声明。图表渲染我用的方案是echarts的微信小程序定制版也就是ec-canvas组件。这个组件本质是echarts核心库跑在一个canvas上小程序里没有完整的DOM环境echarts官方专门出了一个适配版本。使用方法如下第一步把ec-canvas组件目录放到项目的components下面然后在页面的json文件里声明{ usingComponents: { ec-canvas: /components/ec-canvas/ec-canvas } }第二步在wxml里放一个图表的容器view classchart-wrapper ec-canvas idtrend-chart canvas-idtrend-chart ec{{ trendEc }}/ec-canvas /view第三步在js里初始化图表配置import * as echarts from ../../components/ec-canvas/echarts; function initTrendChart(canvas, width, height, dpr) { const chart echarts.init(canvas, null, { width, height, devicePixelRatio: dpr }); canvas.setChart(chart); chart.setOption({ tooltip: { trigger: axis }, xAxis: { type: category, data: [周一,周二,周三,周四,周五,周六,周日] }, yAxis: { type: value }, series: [{ type: line, data: [12, 15, 10, 18, 22, 20, 14], smooth: true }] }); return chart; } Page({ data: { trendEc: { onInit: initTrendChart } } })这里有一个比较隐蔽的问题ec { onInit }这种写法下echarts.init是在onInit回调里执行的所以initTrendChart函数里的echarts对象必须在模块顶部import进来不能在Page({})里局部require否则安卓真机上会报echarts is not defined。我印象很深这个坑在开发者工具里完全不报错一上真机就炸查了很久才找到原因是模块作用域的问题。3.3 移动端性能优化与体验细节小程序的数据分析页面最常见的问题不是功能而是“图表加载太慢、页面滚动卡顿、数据对不上”。这里我把实际优化过的几个点列一下。第一减少setData的数据量。小程序里setData是把数据从逻辑层传到视图层的数据量过大会出现明显卡顿。我封装请求函数的时候后端返回的消费明细列表只截取需要的字段再存进data比如每条明细只保留merchant_name, amount, consumed_at三个字段去掉所有冗余字段。一个10条/页的分页列表setData的数据量控制在几百字节滑动就非常流畅。第二图表数据处理前置到后端。初始化图表时传的xAxis数组和series数组不要在小程序端拼而是在后端直接返回排好序的数组。比如trend接口直接返回{labels: [2024-01, 2024-02, ...], values: [523.5, 689.0, ...]}小程序端直接塞进chart.setOption少写一层数据处理逻辑出错概率大幅下降。第三下拉刷新与缓存策略。数据分析页面的数据理论上没有强实时性要求完全可以做数据缓存。我的做法是请求结果同时wx.setStorageSync存一份带时间戳的副本每次进入页面先读缓存立即渲染再发请求拿新数据对比如果新数据和缓存内容一致就不重新setData。这样用户每次打开页面都是秒开后台静默更新数据。第四小程序分包加载。如果后续功能扩展较多建议把echarts组件包单独放到一个分包里主包只留tabBar页面避免主包超过2M限制。echarts的完整库压缩后也有几百KB对小程序包体来说压力不小。如果只是显示饼图和折线图可以用echarts的按需引入只注册用到的组件包体可以压到300KB以内// 按需引入echarts核心模块 import * as echarts from ../../components/ec-canvas/echarts; // 相当于只加载line、pie这两个图表类型按需引入的实际操作稍微有点绕因为echarts官方的小程序版是预构建好的要自己改构建文件。我在项目里图省事直接用的是完整版echarts毕竟分包之后体积压力不大了。如果你的项目对包体严格敏感再考虑做按需构建。3.4 请求封装与数据绑定小程序端的数据请求不能直接用wx.request裸奔一定要封装一层。我的封装代码很简单核心功能是自动带token、统一错误弹窗、加载态管理、可选缓存。// utils/request.js const BASE_URL http://你的后端地址 function request(path, method GET, data {}, options {}) { return new Promise((resolve, reject) { const token wx.getStorageSync(token) wx.request({ url: ${BASE_URL}${path}, method, data, header: { Content-Type: application/json, Authorization: token ? Bearer ${token} : }, success(res) { if (res.data.code 0) { resolve(res.data.data) } else if (res.data.code 401) { // token失效跳回登录页 wx.removeStorageSync(token) wx.navigateTo({ url: /pages/login/index }) reject(res.data) } else { wx.showToast({ title: res.data.msg, icon: none }) reject(res.data) } }, fail(err) { wx.showToast({ title: 网络异常, icon: none }) reject(err) } }) }) } module.exports { request }封装之后业务页面里调用数据分析接口就非常清爽了const { request } require(../../utils/request) Page({ data: { overview: {} }, async onLoad() { try { const data await request(/api/report/overview/, GET, { month: 2024-05 }) this.setData({ overview: data }) } catch (e) { console.error(加载概览失败, e) } } })统一封装的收益在联调阶段特别明显接口返回结构只要定好code/data/msg三层前端所有请求的异常处理逻辑全部收敛在一个文件里不会出现某个页面忘了处理401导致白屏的尴尬局面。4. 联调、部署与性能排查实际项目里绕不开的硬骨头4.1 前后端联调中的类型与精度问题我实际联调时遇到的最莫名其妙的bug是金额精度问题。django的DecimalField在后端算出来的是Decimal(523.50)这种类型经过JsonResponse序列化会变成字符串523.50。小程序端拿到的是字符串直接用于计算就会出错比如523.50 100的结果不是623.50而是字符串拼接523.50100。这个问题有两个层面的解决方案。第一层是后端统一转类型写一个扩展的JSON encoder在输出前把Decimal转成float。第二层是前端统一用parseFloat包装一下。我最后选择了两边都做# 后端自定义JsonResponse解决Decimal序列化问题 from django.core.serializers.json import DjangoJSONEncoder from django.http import JsonResponse def api_response(code0, dataNone, msgsuccess): return JsonResponse( {code: code, data: data, msg: msg}, encoderDjangoJSONEncoder # 自动处理Decimal和datetime )DjangoJSONEncoder是django内置的JSON编码器能自动把Decimal转成字符串。然后在小程序端的request封装里对返回的data做一次深度遍历把所有看起来像金额的字符串转成数字。更规范的做法是后端统一返回数字类型不要依赖前端判断。后来我实际改成了后端把Decimal手动float()一次再放进data里这样最干脆——前端拿到的data字段全部是number类型不用做任何二次转换。代价是金额展示时如果需要保留两位小数前端得自己toFixed(2)这个开销不大可以接受。4.2 小程序抓包与接口调试技巧小程序页面里的请求直接在开发者工具的Network面板就能看到但这个只适用于开发者工具环境。真机调试或体验版里想看接口请求就得抓包。我实际用来调小程序接口的工具是Charles它的原理是中间人代理手机wifi代理指向电脑的Charles端口Charles再把流量转发到真实服务器。配置步骤是电脑和手机连同一个wifi电脑上Charles开启Proxy - SSL Proxying Settings勾选SSL代理并添加*通配手机wifi设置手动代理填电脑IP加8888端口然后在Charles里就能看到所有经过的HTTPS请求。抓包时有一个关键点小程序默认的HTTPS请求做了证书校验如果Charles的SSL证书没有被手机信任抓到的请求内容会是加密乱码。解决方法是在手机浏览器里访问chls.pro/ssl下载Charles证书并安装。一旦证书装好微信小程序的request请求就能被明文解析了。这个过程不算复杂但是如果不做的话真机上报错errMsg: request:fail你会完全不知道是网络问题、证书问题还是后端接口问题。我在项目调试中还发现一个更轻量的替代方案直接用微信官方的“真机调试2.0”它会在开发者工具里实时打印真机上的console日志和网络请求。虽然不是严格意义的抓包但对于排查“后端返回了但小程序没渲染”这类问题已经足够用。Charles主要用在需要构造篡改请求或者看完整header的场景。4.3 常见问题速查表我踩过的坑都在这问题现象根本原因解决方案小程序请求http://127.0.0.1报错开发者工具未开“不校验合法域名”详情-本地设置-勾选不校验合法域名真机预览时所有请求全部失败微信公众平台未配置request合法域名mp.weixin.qq.com-开发管理-服务器域名中添加HTTPS域名安卓真机echarts报echarts未定义echarts模块在局部作用域引用不到确保在js文件顶部import echarts登录接口偶尔挂起requests请求微信接口未设timeout给requests.get增加timeout5Decimal类型被序列化成字符串JsonResponse默认encoder不支持Decimal改用DjangoJSONEncoder或手动float()金额统计对不上账模拟数据中金额round精度问题用Decimal而不是float存数据库图表x轴显示乱码/日期错位时区换算导致TruncDate结果与本地不符django设置USE_TZFalse或统一用UTC转换下拉刷新后页面数据重复未做分页去重使用create_at游标分页不使用页码跳页其中有几个我要单独展开。一个是时区问题django默认USE_TZTrue数据库存储的是UTC时间但TruncDate分组时django会先按当前时区转换。如果你在同一个项目里有的地方直接用consumed_at展示有的地方用TruncDate分组就会出现“前端显示的是北京时间分组却按UTC日期分”的矛盾。我在项目里直接把USE_TZFalse不仅省心模拟数据灌进去也不用考虑时区换算。另一个是分页问题。消费明细列表用页码分页时如果用户在看第2页的同时刷新了一条数据第2页的数据就会整体错位出现“重复数据”或“漏数据”。我用的是游标分页也就是WHERE consumed_at 上一页最后一条时间 ORDER BY consumed_at DESC LIMIT 20这样即使有新的记录插入也不会影响滚动位置。# 游标分页示例 def detail(request): student request.student cursor request.GET.get(cursor, ) # 上一页最后一条的时间戳 queryset ConsumptionRecord.objects.filter(studentstudent) if cursor: queryset queryset.filter(consumed_at__ltcursor) records queryset.order_by(-consumed_at)[:20] next_cursor records.last().consumed_at if records else None data { list: list(records.values(merchant__name, amount, consumed_at)), next_cursor: next_cursor } return api_response(datadata)4.4 整体部署方案开发机到服务器的最后一步前后端都搞定之后部署是最后一个大坑。小程序必须要求后端是HTTPS域名所以不能随便拿个HTTP的IP地址顶上。我的推荐方案是一台云服务器 nginx gunicorn django域名配SSL证书。步骤大致如下先安装基础环境Python 3.10、MySQL或SQLite。小型项目直接用SQLite就行但考虑到并发写我建议至少用MySQL。安装依赖后manage.py collectstatic收集静态文件然后写gunicorn的启动脚本gunicorn myproject.wsgi:application --bind 127.0.0.1:8000 --workers 3 --timeout 120nginx配置把80端口和443端口的请求转发到gunicorn监听的8000端口同时处理好HTTPS证书server { listen 443 ssl; server_name yourdomain.com; ssl_certificate /etc/nginx/ssl/fullchain.pem; ssl_certificate_key /etc/nginx/ssl/privkey.pem; location / { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }部署完成后再回微信公众平台把请求域名配上。整套流程第一次做大概需要半天但走通以后后续改动只是git pull 重启gunicorn的问题。5. 项目复盘做完这个项目我对数据分析类demo的体会最后分享几个我做完这个项目的直接感受。第一数据分析类小程序项目的核心价值“不在前端图表而在后端能不能把数据算得又快又准”。前端的折线图和饼图任何会echarts的人都能画但后端用TruncDate按天分组、用annotate做消费结构分析、用游标分页避开数据错位这些才是真正体现功底的地方。这也是面试或答辩时最容易展开讲的细节——“我遇到什么问题怎么排查为什么这么解决”。第二模拟数据质量决定了整个开发体验。我第一次灌数时金额和时间完全是纯随机生成画出来的趋势图就是一条直线没有任何分析价值。后来把时间段按用餐规律、周末规律做了加权图表立刻有了“真实感”。做数据分析项目花半小时精心设计模拟数据的分布规律远比你之后对着一条“平直线”找半天bug要值得多。第三小程序的登录和请求封装一定要在最开始就做好。我见过很多项目组开发到一半才开始补统一鉴权结果每个页面都有各自的一套请求逻辑排查问题的时候心态直接崩掉。这个项目里我用的方案虽然简陋但“后端统一响应格式 前端request封装 token自动携带”这三板斧能确保项目从头到尾都不会乱。第四生产环境的坑和本地环境完全不一样。本地跑得好好的接口部署到服务器上可能因为ALLOWED_HOSTS没配置、静态文件路径不对、HTTPS证书链不完整等各种原因挂掉。我的建议是第一次部署留出足够时间把nginx日志和gunicorn日志都开起来看通常服务器端的错误日志能直接告诉你缺了什么。这个项目做完如果还想继续深挖可以往两个方向扩展一是把分析和预测结合比如基于历史消费记录做下月消费预测这就需要引入简单的线性回归或者时间序列分析二是把数据分析能力往“数据大屏”方向走在小程序里内嵌一个更大的报表页。不过这些都是后话把一个分析型小程序从零完整跑通一遍本身就已经覆盖了django ORM、小程序登录、图表渲染、联调部署这一整条全栈链路足够作为课程设计或者个人实战项目的完整履历了。