最近两个月我一直在折腾一个完整的全栈小项目: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.10
- Django 4.2
- Django REST Framework 3.14
- MySQL 8.0(通过 mysqlclient 连接)
- Django SimpleUI(Admin 美化)
- 微信小程序原生框架(基础库版本 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_id=xxx:某角色下的攻略列表
- 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 里的 charset:
DATABASES = { '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_active=True) camp = request.query_params.get('camp') keyword = request.query_params.get('search') if camp: roles = roles.filter(camp=camp) if keyword: roles = roles.filter(name__icontains=keyword) paginator = PageNumberPagination() paginator.page_size = 10 page_roles = paginator.paginate_queryset(roles, request) serializer = RoleListSerializer(page_roles, many=True) 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(pk=pk, is_active=True) except Role.DoesNotExist: return Response({'code': 1, 'msg': '角色不存在'}) Role.objects.filter(pk=pk).update(view_count=F('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_published=True) role_id = request.query_params.get('role_id') keyword = request.query_params.get('search') if role_id: articles = articles.filter(role_id=role_id) if keyword: articles = articles.filter(Q(title__icontains=keyword) | Q(content__icontains=keyword)) articles = articles.order_by('-publish_time') ...order_by('-publish_time') 可能很多人写过,但真正要注意的是,如果攻略数据量大,这个查询会变慢,所以一定要在外键字段和常用筛选字段上建数据库索引。Django 模型里这样声明即可:
class Article(models.Model): role = models.ForeignKey(Role, on_delete=models.CASCADE, db_index=True) title = models.CharField(max_length=200) ...我在实际项目中就因为没有给 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_root=settings.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-headers:
pip 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_index=True 后重新迁移 |
| 后台图片上传后前端访问 404 | MEDIA_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 用扎实,把列表和详情两个核心页面打磨流畅,数据量控制在可以手工维护的范围内。等跑通了,再考虑加用户收藏、评论互动、分享海报这些锦上添花的功能。任何花哨功能都不如一个“打开快、内容准、标题清”的基础版本更能留住用户。