
最近两个月我一直在折腾一个完整的全栈小项目Python Django 做后端接口微信小程序做前端专门做“逃跑吧少年”这款游戏的角色介绍与攻略系统。从数据库建模到接口联调再到小程序真机预览、上线备案整套流程走下来踩了不少坑也积累了不少可以直接抄作业的经验。今天就把这个项目的完整拆解和实现过程整理出来给正在做类似游戏攻略类小程序的朋友当一份实战参考。这个系统的定位很明确玩家打开小程序能看到游戏角色列表、角色详情、技能介绍、上手难度、推荐打法还可以按阵营或关键词筛选角色浏览对应角色的攻略文章。说白了这就是一个“角色图鉴 攻略资讯”的轻量内容社区。适合用 Django 快速搭建管理后台用小程序作为内容展示终端开发量不大但涉及的知识点非常密集模型设计、REST 接口、分页搜索、后台美化、小程序动态标题、备案上线等等都有。1. 项目概述与需求拆解1.1 这个项目到底是做什么的先把这个系统的业务逻辑说清楚。整个项目的载体是“逃跑吧少年”这款非对称对抗手游游戏里有逃生者和追捕者两个阵营每个角色都有独立的技能释放方式、定位倾向和上手难度。新手玩家最大的痛点就是不知道选哪个角色、怎么搭配技能、有哪些进阶打法。我们这个系统要解决的就是这个问题把角色信息结构化整理配合攻略内容做成一个方便查询和浏览的小程序。所以项目本质上是一个内容管理系统CMS只不过展示端是小程序而不是 Web 页面。管理员通过 Django Admin 录入角色资料、编辑攻略文章普通用户在小程序端浏览列表、搜索角色、进入详情页查看攻略。两端数据通过 JSON 接口打通整个链路是MySQL 数据库 - Django 模型 - 视图接口 - 小程序请求 - 页面渲染。1.2 核心功能清单与业务边界结合实际需求我把系统拆成了下面这几个核心模块角色图鉴展示所有角色支持按阵营逃生者/追捕者筛选、按关键词搜索角色名称。角色详情包含角色头像、阵营、定位、上手难度、技能说明、技能图标、推荐道具、玩法思路。攻略文章每个角色下挂多篇攻略按发布时间排序支持文章详情浏览。后台管理使用 Django Admin 做角色和攻略的增删改查并做了简单的界面美化。数据统计记录角色列表浏览量、详情页访问量方便后续做热门角色排行。这里要特别强调一下业务边界。一开始有人建议在这个系统里加上用户注册、评论点赞、金币任务我全部砍掉了。攻略类小程序的第一个版本核心是“内容能查、能看、能管”而不是做社交互动。用户体系和互动功能会让开发量翻倍也会在小程序审核时引入更多合规问题比如用户生成内容需要内容安全接口这个在个人主体的小程序上是比较麻烦的。所以第一版尽量做轻把内容展示和管理做好就已经足够实用了。1.3 为什么用 Django 小程序这套组合选型的时候其实也考虑过 Node.js 和 uniapp但最后还是定成了 Python Django 微信小程序原因很实际。Django 自带 Admin 后台这对纯内容维护来说太重要了。录角色、写攻略、传图片这些重复性操作如果不借助现成的后台框架光前端页面就得写很久。Django 的 ORM 模型能直接帮你建表、改表、迁移我只需要用 Python 定义类数据库表结构就出来了省下了大量写 SQL 的时间。再加上 Django REST Framework 这种成熟库实现 JSON 接口基本就是几行代码的事。小程序端选择微信小程序原生开发没有用 uniapp 或者 Taro是因为这个项目页面复杂度不高原生开发调试最直接而且微信开发者工具的“真机预览”和“体验版”机制很成熟带上手机就能测。对于没有任何小程序经验的朋友来说原生框架反而是学习成本最低的路径。2. 技术选型与数据建模2.1 技术栈明细与版本选择先把我实际使用的技术栈列出来版本号也写清楚因为这几个版本搭配在一起我自己验证过可以少踩很多兼容性坑Python 3.10Django 4.2Django REST Framework 3.14MySQL 8.0通过 mysqlclient 连接Django SimpleUIAdmin 美化微信小程序原生框架基础库版本 3.x本地图片存储后续量大了再上对象存储为什么选 Python 3.10 而不是最新的 3.12主要是因为 mysqlclient 在 3.12 上的编译环境兼容性偶尔会出问题而在 3.10 和 Django 4.2 的组合下安装非常顺滑。Django 4.2 是 LTS 版本支持周期长网上资料也多遇到问题搜索到的概率更大。2.2 数据库表设计与模型实现数据建模是这个项目的核心我设计了三张主表角色表、攻略表、标签表另外做了角色和标签的多对多关系。角色表的字段大概是这样name角色名称camp阵营逃生者 / 追捕者role_type定位比如突击、辅助、控制、防守等difficulty上手难度用 1 到 5 表示avatar角色头像图片skill_name技能名称skill_desc技能描述skill_icon技能图标recommend_items推荐道具用逗号分隔存储play_tips玩法思路TextField 存储长文本view_count浏览量统计攻略表则关联角色表role外键关联角色title攻略标题content攻略正文cover封面图author作者publish_time发布时间is_published是否发布用于控制前端展示Django 模型代码也很直观重点在于把外键关系和多对多关系定义清楚。2.3 API 接口设计思路接口层面我统一返回 JSON并且约定了一个固定格式code 为 0 表示成功非 0 表示失败msg 是提示信息data 才是真正业务数据。这样小程序端处理返回结果时逻辑非常统一不管请求哪个接口只需要判断 code。主要接口有这几个GET /api/roles/角色列表支持 camp 筛选和 search 关键词搜索支持分页GET /api/roles/ /角色详情GET /api/articles/?role_idxxx某角色下的攻略列表GET /api/articles/ /攻略详情分页我用了 DRF 的 PageNumberPagination每页数量设为 10。这里需要注意小程序端请求的时候要传 page 参数返回的 data 里除了列表数据还要带上 total 和 has_next 字段方便前端用“加载更多”的方式处理分页。3. 后端 Django 核心功能实现3.1 项目初始化与 App 结构实际创建工作我是这么做的django-admin startproject escape_game cd escape_game python manage.py startapp roles python manage.py startapp articles然后去 settings.py 里做基础配置注册应用、配置 MySQL、设置语言和时区为 zh-hans 和 Asia/Shanghai、配置 STATIC_URL 和 MEDIA_URL。关于 MySQL 的配置经常有人漏掉一个关键点就是 OPTIONS 里的 charsetDATABASES { default: { ENGINE: django.db.backends.mysql, NAME: escape_game, USER: root, PASSWORD: your_password, HOST: 127.0.0.1, PORT: 3306, OPTIONS: { charset: utf8mb4, }, } }如果这里不指定 utf8mb4中文数据写入后可能出现乱码或者出现“Incorrect string value”的错误。这个细节在很多零散教程里都不提但我实测下来非常关键。3.2 角色列表与详情接口开发接口我用了 DRF 的 APIView 来写没有引入 ViewSet。原因很简单这个项目的接口数量不多APIView 的写法更直白逻辑也都集中在函数里调试起来很清晰。角色列表接口的核心逻辑class RoleListView(APIView): def get(self, request): roles Role.objects.filter(is_activeTrue) camp request.query_params.get(camp) keyword request.query_params.get(search) if camp: roles roles.filter(campcamp) if keyword: roles roles.filter(name__icontainskeyword) paginator PageNumberPagination() paginator.page_size 10 page_roles paginator.paginate_queryset(roles, request) serializer RoleListSerializer(page_roles, manyTrue) return Response({ code: 0, msg: success, data: { list: serializer.data, total: paginator.page.paginator.count, has_next: paginator.page.has_next(), } })这里我用了一个小技巧列表接口的序列化器只返回 id、名称、头像、阵营、难度这几个字段而详情接口的序列化器才返回全部字段。因为列表页只需要展示卡片信息全字段返回会浪费流量小程序端渲染也会慢一些。详情接口要处理点击量的自增这里注意不能直接赋值class RoleDetailView(APIView): def get(self, request, pk): try: role Role.objects.get(pkpk, is_activeTrue) except Role.DoesNotExist: return Response({code: 1, msg: 角色不存在}) Role.objects.filter(pkpk).update(view_countF(view_count) 1) serializer RoleDetailSerializer(role) return Response({code: 0, msg: success, data: serializer.data})这里用 F 表达式做自增是为了避免并发情况下先读后写导致计数丢失这是数据库并发操作最基本的坑。3.3 攻略搜索与分类筛选攻略接口需要同时支持“按角色查”和“按关键词查”以及按发布时间排序。我用了 Django 的 Q 对象来实现多条件组合class ArticleListView(APIView): def get(self, request): articles Article.objects.filter(is_publishedTrue) role_id request.query_params.get(role_id) keyword request.query_params.get(search) if role_id: articles articles.filter(role_idrole_id) if keyword: articles articles.filter(Q(title__icontainskeyword) | Q(content__icontainskeyword)) articles articles.order_by(-publish_time) ...order_by(-publish_time) 可能很多人写过但真正要注意的是如果攻略数据量大这个查询会变慢所以一定要在外键字段和常用筛选字段上建数据库索引。Django 模型里这样声明即可class Article(models.Model): role models.ForeignKey(Role, on_deletemodels.CASCADE, db_indexTrue) title models.CharField(max_length200) ...我在实际项目中就因为没有给 role_id 建索引导致某一次联调时接口响应要 1.5 秒加了索引后直接降到 60 毫秒。数据量小的时候感觉不到一旦超过几万条索引就是必需品。3.4 Django Admin 后台管理与界面美化Django Admin 原生界面的确朴素了些而且默认搜索和筛选不够方便。我做了两件事一是用 Django SimpleUI 美化整体界面二是在 admin.py 里加强列表页的展示和搜索能力。SimpleUI 的接入很简单先安装pip install django-simpleui然后在 settings.py 的 INSTALLED_APPS 里必须把 simpleui 放在 django.contrib.admin 前面INSTALLED_APPS [ simpleui, django.contrib.admin, ... ]不按这个顺序放页面样式可能加载不出来。接着在 admin.py 里优化列表页class RoleAdmin(admin.ModelAdmin): list_display (name, camp, role_type, difficulty, view_count) list_filter (camp, role_type) search_fields (name, skill_name) list_per_page 20特别是 list_filter 和 search_fields有了它们后台录入角色后再去修改某个角色信息就不用在长长的列表里一页一页翻了。这个体验提升对日常运维来说非常明显。后台能直接上传图片也是我选择 Admin 做管理端的原因之一Django 自带的 FileField 配合 MEDIA_URL图片上传后自动存储前端直接拿拼接好的完整 URL 渲染即可。4. 小程序前端关键开发4.1 小程序项目结构与公共请求封装小程序端我按照功能做了文件目录划分pages/home首页角色列表pages/detail角色详情pages/articles攻略列表pages/article-detail攻略详情utils/request.js请求封装utils/config.js接口地址配置公共请求封装是一件值得花时间做好的事因为项目里所有接口调用都会走它。我的 request.js 核心逻辑是封装 Promise统一管理 baseURL、超时时间和错误处理const BASE_URL http://127.0.0.1:8000; function request(path, method GET, data {}) { return new Promise((resolve, reject) { wx.request({ url: BASE_URL path, method: method, data: data, header: { Content-Type: application/json }, timeout: 8000, success(res) { if (res.statusCode 200 res.data.code 0) { resolve(res.data.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 };这里有几个细节值得注意。timeout 一定要设置否则弱网环境下请求会一直挂着用户会以为小程序卡死了。错误处理里用了 wx.showToast 但这种提示不能太频繁我这版是每次失败都弹因为还在开发调试阶段方便发现问题。如果后面正式上线建议对错误弹窗做节流避免并发请求一起挂掉的时候弹窗刷屏。4.2 首页角色列表与加载更多首页角色列表我用了“下拉刷新 触底加载更多”的经典交互。核心逻辑在 onLoad 里拉第一页数据触底时判断 has_next 再拉下一页let currentPage 1; function loadRoles(reset false) { if (reset) { currentPage 1; setData({ roleList: [], hasMore: true }); } request(/api/roles/?page${currentPage}) .then(data { setData({ roleList: reset ? data.list : this.data.roleList.concat(data.list), hasMore: data.has_next }); if (data.has_next) { currentPage; } }); } onPullDownRefresh() { loadRoles(true); wx.stopPullDownRefresh(); } onReachBottom() { if (this.data.hasMore) { loadRoles(); } }列表页布局我是用 flex 加两列网格做的角色卡片每张卡片显示头像、名称、阵营标签和难度星级。这里需要注意小程序里图片的加载模式和缓存策略直接决定了页面会不会卡。角色头像建议统一用 300x300 的图参考 icon 尺寸前端 image 标签开启 lazy-load 属性图片只按需加载。我第一次没开 lazy-load首屏 20 个角色头像一下子全请求弱网环境下页面白屏了差不多 3 秒体验非常差。4.3 角色详情页与动态标题设置角色详情页我是用角色 id 从列表页跳转过去的页面 onLoad 接收 options.id然后请求详情接口。这里有个热搜词是“小程序动态设置标题”恰好是这个页面的一个重点。打开角色详情后顶部导航栏不应该一直显示“详情”而应该动态变成角色名不然用户分不清自己在看哪个角色。微信小程序提供了 wx.setNavigationBarTitle 接口我们可以在详情接口返回后动态设置onLoad(options) { this.roleId options.id; request(/api/roles/${this.roleId}) .then(data { this.setData({ role: data }); wx.setNavigationBarTitle({ title: data.name }); }); }这个功能虽然是一行代码但很提观感。用户从首页点进详情顶部标题跟着角色名变化整体感觉就非常“定制化”。详情页内部我自己比较得意的是技能展示区。技能说明文字往往比较长我默认只展示前 60 个字后面加一个“展开”按钮点击后再展示全部。这在小程序里是一个很常见的交互逻辑也很简单用 data 里的 expanded 字段控制点击时取反。不过要注意 setData 的数据量如果技能文本特别长建议截断后再 setData不要整段文本都塞进去容易出现小程序 setData 性能告警。4.4 搜索与筛选功能实现首页顶部我放了一个搜索框和一个阵营筛选按钮组。搜索框的交互比较讲究既有“输入关键词后点搜索按钮提交”的方式也有“输入即搜索”的实时方式。考虑到后端接口是查询数据库实时搜索如果每个字符都发一次请求会给服务器造成不小压力所以我实现的是点击搜索按钮或者键盘确认时才发起请求。筛选按钮组这里我用了微信小程序的 radio 模拟三个选项全部 / 逃生者 / 追捕者。每次切换就重新请求第一页数据并把列表重置handleCampChange(e) { const camp e.currentTarget.dataset.camp; this.setData({ currentCamp: camp }); currentPage 1; loadRoles(true); }这里有个要注意的地方用户筛选后如果滚动到很下面再点另一个筛选按钮需要手动把页面滚回顶部。我踩过这个坑当时筛选后列表从新数据开始渲染但滚动位置还停留在旧位置用户看着非常困惑。解决方法是调用 wx.pageScrollTo({ scrollTop: 0 })。5. 联调、部署与细节优化5.1 本地联调Django 开发服务器加小程序开发者工具本地联调是整个开发过程中最需要顺手配置好的一环。Django 开发服务器默认跑在 127.0.0.1:8000小程序开发者工具里如果不做设置直接请求这个地址会报错。需要在小程序开发者工具的“详情 - 本地设置”里勾选“不校验合法域名、web-view业务域名、TLS 版本以及 HTTPS 证书”这样开发阶段就能访问本机接口。还有一个很隐蔽的问题就是真机预览的时候不能访问电脑上的 127.0.0.1因为手机访问的是它自己。解决办法是让电脑和手机处于同一局域网把 Django 开发服务器的启动参数改成python manage.py runserver 0.0.0.0:8000然后在小程序 request.js 里把 BASE_URL 换成电脑的局域网 IP比如 http://192.168.1.8:8000。注意 DEBUG 模式下 Django 的 ALLOWED_HOSTS 要看情况加入局域网 IP否则会返回 400。我是有一次真机调试页面死活拉不到数据最后发现就是 ALLOWED_HOSTS 的问题。从那以后我就在 settings.py 里统一配置成了ALLOWED_HOSTS [*]开发阶段这样没有大问题但部署上线时务必改成真实的域名。5.2 多媒体资源处理与静态文件游戏角色介绍必然要处理大量图片角色头像、技能图标、攻略封面。我在开发阶段直接用 Django 的 MEDIA_URL 做本地存储注意在项目的 urls.py 里加上静态资源路由from django.conf import settings from django.conf.urls.static import static urlpatterns [ ... ] if settings.DEBUG: urlpatterns static(settings.MEDIA_URL, document_rootsettings.MEDIA_ROOT)这里的关键在于 MEDIA_ROOT 要设置为一个独立目录包括完整路径不能只写相对路径否则不同启动目录下图片可能存到不同的位置代码一提交别人拉下来就找不到图片了。图片尺寸和质量控制也是我后来才发现的重要优化点。一开始我从游戏官网截图直接上传一张图动辄 1MB 多小程序端加载非常慢。后来在后台维护时统一要求先通过在线图片压缩工具压到 200KB 以内再上传使用。这个流程看起来有点笨但对小程序首屏速度和字节跳动式的手感提升立竿见影。5.3 小程序备案与上线注意点现在微信小程序上线前需要完成备案这个是绕不开的流程。备案时“项目名称”和“服务内容”要如实填写比如这个项目就写“游戏攻略信息展示”服务内容选择“信息查询”基本上不会出问题。备案信息里有一个“小程序简介”字段建议写清楚具体服务内容比如“提供逃跑吧少年游戏角色图鉴与攻略内容查询”不要写太宽泛的“游戏资讯平台”否则被要求补充材料的概率会更大。我第一次就是简介写得太大而被打回修改过一次补了材料才过耽误了差不多一周。关于类目选择个人主体的小程序做游戏攻略类内容一般选“工具 - 信息查询”或者“生活服务 百科”这类普通类目不要碰“游戏”类目因为游戏类目需要版号资质个人开发者根本拿不到。我们在做内容分类时也要注意这一点系统本身只是信息展示工具不是游戏下载平台上线审核时问题就不大。6. 常见问题排查与避坑实录6.1 mysqlclient 安装失败这个问题出现的频率非常高尤其是在 Windows 环境下pip install mysqlclient 经常会报错提示找不到 MySQL Connector/C。实际原因是 mysqlclient 需要编译 C 扩展Windows 上没有完整的编译环境就会失败。我个人的解决方案是直接安装预编译的 whl 包。先去一个专门收集 Windows 二进制包的网站下载对应 Python 版本的 mysqlclient whl 文件然后本地安装pip install mysqlclient-2.2.0-cp310-cp310-win_amd64.whl安装完成后在 Python 命令行里执行 import MySQLdb能正常导入就说明连接 MySQL 的环境已经通了。这里有个小技巧即使你后端只写 Django ORM不直接写 SQL也必须保证这个 MySQLdb 模块能正常导入因为 Django 连接 MySQL 时底层要依赖它。6.2 跨域问题小程序里 wx.request 默认情况下是不存在浏览器跨域问题的因为小程序的请求是宿主环境发起的不受浏览器的同源策略限制。但如果你用网页调试器测试接口或者以后要接一个 H5 版作为补充跨域问题就一定要处理。我提前在 Django 里加了 django-cors-headerspip install django-cors-headers然后在 settings.py 里配置中间件MIDDLEWARE [ ... corsheaders.middleware.CorsMiddleware, ... ] CORS_ALLOW_ALL_ORIGINS True开发阶段直接允许所有来源是没问题的但上线以后建议改成具体的域名白名单。有人可能会问小程序不是没有跨域吗为什么还要配。我的想法很简单后端接口以后不一定只服务小程序这一个客户端接 Web 管理端、接活动页面都需要跨域支持提前配好省得后面临时加。6.3 小程序 setData 性能与图片缓存小程序的长列表最容易踩的坑就是海量数据一次性 setData。如果你列表接口返回了 50 条数据直接在 onLoad 里 setData({ list: 50 条数据 })在小程序开发者工具里经常会看到警告说 setData 的数据过大。我最后采用的解决方案就是前面提到的分页加载每页 10 条触底再加载下一页。同时每条数据里只保留前端展示必需的字段那些大段的技能描述、攻略正文都不放进列表接口只在详情接口里返回。用数据结构的优化去切掉性能隐患比后期用虚拟列表组件去补坑要轻松得多。图片缓存方面wx.request 返回的图片 URL 如果指向同一个资源小程序底层会做一定的缓存但角色头像随角色数据更新的时候可能因为 URL 不变导致头像还是旧的。我踩过这个坑最后是在图片 URL 后面拼接一个时间戳查询参数强制刷新const url data.avatar ?t Date.now();但这个只在后台明确更新了图片的时候才需要做平时不建议加时间戳因为会破坏缓存每次进来都要重新下载图片。6.4 常见问题速查表我在开发过程中遇到的问题远不止上面几个这里整理成一张速查表都是我实际碰到并验证过解决方法的现象可能原因解决办法接口返回 500 错误数据库没迁移或模型字段不存在执行 python manage.py makemigrations 和 migrate列表接口超时外键字段没建索引模型字段加 db_indexTrue 后重新迁移后台图片上传后前端访问 404MEDIA_URL 路由没配置settings.DEBUG 下在 urls.py 添加 static 路由真机预览请求失败手机访问不到电脑 IP改为 runserver 0.0.0.0:8000并填对局域网 IP小程序提示 URL 不在合法域名列表上线后没有配置 request 合法域名在小程序管理后台配置已备案的 HTTPS 域名首页加载白屏图片没开懒加载或图片体积过大图片压缩到 200KB 以内image 加 lazy-load角色名称显示乱码数据库连接 charset 设置错误DATABASES 里 OPTIONS 指定 charset 为 utf8mb4写在后面的项目复盘做个简单的复盘。这个项目从零到一跑通我最大的感受是游戏攻略类小程序并没有想象中那么复杂核心难点不在功能而在于内容的组织方式和数据的一致性。Django 的模型设计如果前期没想清楚后面改起来会非常痛苦。比如我在一开始没有把“角色”和“攻略”拆成两张表而是把攻略内容直接塞在角色表里结果详情页接口和列表页接口的返回结构互相纠缠前端改了半天。后来拆成两张表接口各管各的逻辑立刻清爽了很多。这算是踩了不少坑之后最真实的一个体会。另外小程序端开发时所有接口一定要用固定的数据格式约定特别是 code、msg、data 这套结构前后端自始至终保持完全一致。哪怕少一个字段前端就要写额外的容错代码一旦字段多了维护成本更是成倍上涨。如果你正准备做一个类似的项目我的建议是第一版老老实实做内容展示把 Django Admin 用扎实把列表和详情两个核心页面打磨流畅数据量控制在可以手工维护的范围内。等跑通了再考虑加用户收藏、评论互动、分享海报这些锦上添花的功能。任何花哨功能都不如一个“打开快、内容准、标题清”的基础版本更能留住用户。