资讯详情

开源CMS前后端分离实战:内容模型、栏目树、权限与静态化避坑

📅 2026/10/31 8:00:44 | 华诺云谱 👁 阅读
开源CMS前后端分离实战:内容模型、栏目树、权限与静态化避坑
简介这是一套免费开源的CMS内容管理系统面向需要快速搭建网站、学习内容管理架构或进行二次开发的个人与企业用户提供前后台代码可用于内容发布、用户与权限管理、前台展示等常见建站场景。压缩包约13.38MB文件总数与主要类型明细暂未提供实际内容以压缩包为准从描述看涉及Java技术栈与database数据库目录适合结合MySQL等关系型数据库理解内容存储、索引优化与访问结构。已有244人学习说明其在建站入门与CMS源码研究方面具有一定参考价值。读者可借此了解免费开源CMS的许可协议、前后台职责划分和二次开发入口按需扩展功能、调整界面或优化性能同时注意遵守GPL、Apache等开源许可证及商业使用条款。整体更适合具备基础Web开发知识、希望从源码层面掌握CMS运行机制的人群。1. 装一套CMS就能把网站交给运营自己更新这个想法我见过不止一个团队栽在上面真正卡住人的往往不是编辑器够不够好用而是内容模型建得对不对、栏目树撑不撑得住三级导航、发布之后前台多久能看到变化。这套开源的 cms 内容管理系统走的是前后台分离的路子后台一套独立管理端管内容录入、栏目维护、权限分配前台一套展示站管模板渲染、URL 路由和静态化。前台后台是分开的两个工程网站代码互不干扰本地起一遍十几分钟。它适合中小团队官网、资讯站、企业多栏目站点也适合想拿现成前后台代码做二次开发的开发者——增删改查、附件上传、富文本、角色权限这些脏活已经铺好了你只需要关心业务字段和模板长什么样。内容量在百万级往上、需要复杂审批流编排的场景这套结构会显得单薄。2. 技术栈与目录结构先把项目骨架摸清楚再动手拿到一份前后台代码很多人第一反应是找 README 里的启动命令直接跑跑挂了再回头翻目录。我更习惯反过来先把工程切成几块搞清楚每块负责什么、彼此靠什么通信再决定从哪启动。这套 CMS 的边界划得比较干净三个工程各管一摊读懂目录基本就懂了一半架构。2.1 技术栈前台为什么不做单页应用后台为什么必须做前台用的是 Python 的 Web 框架加模板渲染没有上 React、Vue 那套同构方案。这个选择在内容站场景下很务实页面结构由栏目和模板决定交互只占很小一部分模板直出意味着首屏没有白屏等待搜索引擎抓取拿到的就是完整 HTML运维上也不用为 Node 运行时单独准备一套进程。常见做法是把列表页、详情页、单页各写一个模板栏目表里存一个template字段指过去运营换模板不用改代码。后台则是标准的 Vue 3 单页应用配 Element Plus 做表单和表格Vite 打包。后台的交互密度和前台完全不是一个量级——拖拽排序、富文本、批量操作、树形栏目选择这些用模板渲染写起来会很痛苦。前后台通过 REST 接口通信后台只认 JSON不碰模板。中间层是三样东西MySQL 存业务数据Redis 扛缓存和会话Nginx 在最前面做反向代理和静态文件分发。附件不落应用服务器本地磁盘走独立的上传目录或者对象存储这一条决定了后面能不能水平扩容。提示目录里如果看到requirements.txt和package.json同时存在说明前后端确实是两个独立依赖体系别指望一条命令全装完。2.2 目录结构三个工程各自的边界熟悉这套结构比记任何命令都管用二次开发时你要改的文件基本都能从目录名直接定位cms/ ├── backend/ # 后端工程 │ ├── config/ # 全局配置settings 分环境、根路由、wsgi │ ├── apps/ │ │ ├── content/ # 内容域栏目、文章、标签、内容类型 │ │ ├── accounts/ # 账号域用户、角色、权限码 │ │ ├── media_lib/ # 附件域图片库、文件上传、缩略图 │ │ ├── site_config/ # 站点域站点信息、导航、友情链接、SEO 默认值 │ │ └── api/ # 接口层DRF 视图、序列化器、路由注册 │ ├── templates/ # 前台模板list.html / detail.html / page.html │ ├── static/ # 前台静态资源css、js、font │ ├── fixtures/ # 初始化数据站点配置、示例栏目、默认角色 │ └── manage.py ├── admin-web/ # 后台管理端工程 │ ├── src/ │ │ ├── api/ # 接口封装一个模块一个文件 │ │ ├── views/ # 页面内容列表、栏目树、角色、附件库 │ │ ├── router/ # 路由与菜单配置权限码在这里挂 │ │ └── store/ # 登录态、当前站点、字典缓存 │ └── vite.config.js └── deploy/ ├── nginx.conf # 反代规则 静态目录 └── docker-compose.yml # 应用、数据库、缓存三段apps/下面按业务域切分而不是按技术层切分这一点对二次开发很关键。你要加一个新的内容类型改的是content/下面的一处注册而不是在十几个views.py、models.py里到处翻。接口层单独抽成api/好处是模型和序列化器解耦同一个模型可以对外暴露两套字段不同的接口——后台管理用全字段前台展示用裁剪过的字段。templates/目录在 README 里通常提得不多但它是前台能不能改出花样的关键。栏目表里的template字段存的是相对路径比如list_news.html改模板就新建一个文件把栏目指过去不用动视图。2.3 配置与首次启动从解压到登录后台配置文件走的是.env加settings分环境的组合。settings/下一般拆成base.py、dev.py、prod.pybase.py只写通用项数据库、缓存、密钥这些都从环境变量读。这样做的好处是同一份代码在本地和服务器上只差一个.env文件不会出现「本地能跑线上炸」的经典问题。# 1. 后端依赖 cd backend python -m venv .venv source .venv/bin/activate # Windows 用 .venv\Scripts\activate pip install -r requirements.txt # 2. 环境变量照 example 复制一份再改 cp .env.example .env # 至少改这几项DB_HOST / DB_NAME / DB_USER / DB_PASSWORD / REDIS_URL / SECRET_KEY # 3. 建表并灌初始数据 python manage.py migrate python manage.py loaddata fixtures/init_site.json python manage.py createsuperuser # 4. 起后端服务 python manage.py runserver 0.0.0.0:8000migrate负责按模型建表loaddata把fixtures/init_site.json里的站点配置、默认角色、示例栏目导入进去——这一步别跳过后台很多页面依赖站点配置存在缺了会白屏。createsuperuser建的是超级管理员权限全开用于第一次登录。# 后台管理端 cd admin-web npm install npm run dev # 默认 5173 端口vite.config.js里需要一个代理否则后台发出去的/api/xxx会打到 5173 自己身上// admin-web/vite.config.js import { defineConfig } from vite import vue from vitejs/plugin-vue export default defineConfig({ plugins: [vue()], server: { port: 5173, proxy: { // 所有 /api 开头的请求转发给后端避免开发期跨域 /api: { target: http://127.0.0.1:8000, changeOrigin: true, }, // 开发期直接访问后台上传的图片也要能出来 /media: { target: http://127.0.0.1:8000, changeOrigin: true, }, }, }, })changeOrigin的作用是把请求头里的 Host 改写成目标地址后端做域名校验或者生成绝对链接时会用到。代理配好后前端代码里所有请求都写相对路径/api/...线上切到 Nginx 反代时一行不用改。这是我在所有前后端分离项目里都坚持的一条前端代码里永远不出现http://localhost:8000这种硬编码。前台访问http://127.0.0.1:8000/就能看到展示站后台访问http://127.0.0.1:5173/用刚才建的超级管理员登录。两边都起来之后先别急着往里塞内容去后台的「站点配置」把站点名、域名、SEO 默认值填一遍——后面静态化和绝对链接生成都要读这几个值。3. 后台核心模块内容模型、栏目树和权限三件套后台页面看起来功能很多剥开之后核心就三块内容怎么存、栏目怎么组织、谁能干什么。这三块的设计质量直接决定这套 CMS 能不能撑住真实业务。我见过不少半成品项目在这三处偷懒结果就是运营提一个「新闻和产品要用不同字段」的需求开发得改三天。3.1 内容模型一张主表加 JSON 扩展字段最朴素的做法是每种内容建一张表新闻一张、产品一张、案例一张。等到要加「视频」类型的时候列表页、搜索、标签、附件关联全都要复制一遍。这套 CMS 用的是主表加扩展字段的方案所有内容共用一张表差异部分塞进 JSON 字段。# backend/apps/content/models.py from django.db import models from django.conf import settings class Article(models.Model): 内容主表新闻、产品、案例、单页共用差异字段放 extra STATUS_CHOICES ( (draft, 草稿), (review, 待审), (published, 已发布), (offline, 已下线), ) # 内容类型标识对应 content_types 注册表里的 key content_type models.CharField(内容类型, max_length32, db_indexTrue) title models.CharField(标题, max_length200) # slug 用于生成前台 URL同一栏目下必须唯一 slug models.SlugField(URL 标识, max_length200, db_indexTrue) category models.ForeignKey( content.Category, verbose_name主栏目, on_deletemodels.PROTECT, # 栏目下还有内容时禁止删除避免误操作丢数据 ) summary models.TextField(摘要, max_length500, blankTrue) body models.TextField(正文 HTML, blankTrue) cover models.CharField(封面图地址, max_length255, blankTrue) # 扩展字段价格、规格、作者、来源等按内容类型自定义 extra models.JSONField(扩展字段, defaultdict, blankTrue) status models.CharField(状态, max_length16, choicesSTATUS_CHOICES, defaultdraft) is_top models.BooleanField(置顶, defaultFalse) sort models.IntegerField(排序值, default0, db_indexTrue) author models.ForeignKey( settings.AUTH_USER_MODEL, nullTrue, blankTrue, on_deletemodels.SET_NULL, verbose_name创建人, ) published_at models.DateTimeField(发布时间, nullTrue, blankTrue, db_indexTrue) created_at models.DateTimeField(auto_now_addTrue) updated_at models.DateTimeField(auto_nowTrue) class Meta: ordering [-is_top, sort, -published_at, -id] indexes [ models.Index(fields[content_type, status, published_at]), models.Index(fields[category, status]), ] constraints [ # 同一栏目下 slug 唯一跨栏目可以重名 models.UniqueConstraint( fields[category, slug], nameuniq_slug_per_category ) ]几个设计点值得展开说。content_type是一个字符串标识而不是外键原因是内容类型的定义在代码里一个 Python 字典注册表不在数据库里这样加类型不用改表结构。category用PROTECT而不是CASCADE栏目删除时如果有内容挂着会直接报错——这个限制在运营手里是救命的设计血泪经验是有人误删过一个栏目联级下去带走两百多篇文章。extra用 JSON 字段而不是再建一张扩展表好处是查询和写入都只有一次坏处是不能对扩展字段建索引。判断标准很简单如果某个扩展字段需要高频筛选排序就把它提升成主表字段如果只是详情页展示留在 JSON 里最省事。常见做法是在序列化器里按内容类型定义一份字段白名单写 JSON 时做一次校验防止前端塞进来乱七八糟的键。slug的唯一约束带上category是个细节。全局唯一会逼着运营给每篇文章想一个不重名的英文串体验很差按栏目唯一则允许「产品A」和「新闻A」各自有一个a的 slugURL 前缀不同不会冲突。3.2 栏目树用路径字段换掉递归查询栏目要支持无限级父子关系用parent自关联最直观但每次查子树都要递归页面上随便挂个导航就要打十几次库。这套代码的做法是在栏目表上多存两个字段path存祖先链level存层级。# backend/apps/content/models.py class Category(models.Model): name models.CharField(栏目名, max_length50) slug models.SlugField(URL 标识, max_length80) parent models.ForeignKey( self, nullTrue, blankTrue, related_namechildren, on_deletemodels.CASCADE, verbose_name上级栏目, ) # 祖先链形如 0/3/12根栏目为 0 path models.CharField(祖先路径, max_length255, db_indexTrue, default0) level models.PositiveSmallIntegerField(层级, default1) sort models.IntegerField(排序值, default0) is_nav models.BooleanField(显示在导航, defaultTrue) template models.CharField(绑定模板, max_length120, blankTrue) class Meta: ordering [sort, id] def save(self, *args, **kwargs): if self.parent: self.level self.parent.level 1 self.path f{self.parent.path}/{self.parent_id} else: self.level 1 self.path 0 super().save(*args, **kwargs)save里重算path和level保证任何一次保存之后这两个字段都是对的。要注意的是父栏目被移动时子树的path会全部失效需要在视图层加一个「移动后级联刷新」的处理取出以旧路径为前缀的所有栏目统一替换前缀。这一步少了导航树会显示正常但列表页查不到数据属于典型的静默故障。查子树就变成一次前缀匹配# backend/apps/content/services.py def subtree_ids(category): 返回该栏目及其全部子孙栏目的 ID用于列表页聚合内容 if category is None: return [] prefix f{category.path}/{category.id} ids list( Category.objects .filter(models.Q(path__startswithprefix) | models.Q(idcategory.id)) .values_list(id, flatTrue) ) return idspath__startswith会命中path上的索引一次查询拿到整棵子树避免递归。参数category传当前栏目对象返回的是 ID 列表直接喂给Article.objects.filter(category_id__in...)。这里有个边界根栏目的path是0拼出来的前缀是0/1会不会误匹配到0/10会。所以前缀后面要补一个/即f{category.path}/{category.id}/或者像上面这样在存path时保证分隔符格式统一。这个坑我在两个项目里踩过表现出来是「A 栏目的列表页混进了 B 栏目的文章」排查起来很费时间。导航渲染一般是取is_navTrue且level 3的栏目一次性查出来在内存里拼树不要每层查一次库。3.3 权限角色挂权限码接口上挂装饰器后台的权限体系用的是用户-角色-权限码三段式没有做到更细的数据级权限。角色表里存一个权限码数组接口层用装饰器校验前端菜单也读同一份数组来决定显示哪些入口。# backend/apps/accounts/perms.py from functools import wraps from rest_framework.exceptions import PermissionDenied def require_perm(code): 接口级权限校验code 形如 content:article:publish def decorator(view_func): wraps(view_func) def wrapper(self, request, *args, **kwargs): role getattr(request.user, role, None) granted set((role.perms if role else []) or []) # 超级管理员直接放行方便排障 if request.user.is_superuser or code in granted: return view_func(self, request, *args, **kwargs) raise PermissionDenied(f缺少权限{code}) return wrapper return decorator权限码的命名统一成域:资源:动作content:article:publish表示内容域的文章发布权。这套命名在角色配置页上按前缀分组展示运营看起来清楚开发加新接口时也容易判断该挂哪个码。# backend/apps/api/views_article.py class ArticleViewSet(viewsets.ModelViewSet): queryset Article.objects.select_related(category, author) serializer_class ArticleSerializer action(methods[post], detailTrue) require_perm(content:article:publish) def publish(self, request, pkNone): article self.get_object() article.status published article.published_at timezone.now() article.save(update_fields[status, published_at, updated_at]) # 发布后触发前台静态页重建任务丢进队列异步跑 rebuild_static_page.delay(article.id) return Response({code: 0, msg: ok})action定义的是非标准 CRUD 的额外接口detailTrue表示路径上带主键。require_perm放在action下面一行顺序不能反否则装饰器拿不到 DRF 包装后的方法。update_fields限定只写三个字段避免并发保存时把别人改的正文覆盖掉。前端菜单的权限控制在路由配置里// admin-web/src/router/index.js export const menuRoutes [ { path: /content/article, name: ArticleList, component: () import(/views/content/ArticleList.vue), meta: { title: 文章管理, perm: content:article:list }, }, { path: /system/role, name: RoleList, component: () import(/views/system/RoleList.vue), meta: { title: 角色管理, perm: system:role:list }, }, ]路由守卫在登录后拿当前用户的权限码数组过滤掉meta.perm不在数组里的菜单。要注意这只防君子不防小人真正的拦截必须在后端接口上做——前后端各校验一次前端管体验后端管安全。4. 前台渲染链路模板、路由、静态化三段拼起来后台把内容存好了前台怎么把它变成用户看到的页面这条链路是这套 CMS 里最容易被低估的部分。很多人以为写个模板循环输出就完事了等站点上线被搜索引擎收录一轮才发现 URL 结构、缓存、静态化每一样都能单独出问题。4.1 模板渲染视图只管取数据模板只管排版前台视图遵循一个原则视图里不做业务判断只负责把数据装配成上下文丢给模板。列表页、详情页、单页三种视图结构高度相似差别只在查询条件和模板文件。# backend/apps/content/views_front.py from django.core.paginator import Paginator from django.shortcuts import render, get_object_or_404 from .models import Article, Category def category_list(request, slug, page1): category get_object_or_404(Category, slugslug) # 取子树 ID父栏目页面要聚合子孙栏目的内容 ids subtree_ids(category) qs (Article.objects .filter(category_id__inids, statuspublished) .select_related(category) .order_by(-is_top, sort, -published_at)) paginator Paginator(qs, 15) # 每页 15 条 page_obj paginator.get_page(page) # 页码越界自动回落到合法页 context { category: category, page_obj: page_obj, nav_categories: build_nav(), # 顶部导航通常走缓存 seo: { title: category.name, keywords: category.name, description: f{category.name}列表页, }, } # 栏目绑定的模板优先没绑就走默认列表模板 tpl category.template or list.html return render(request, tpl, context)select_related(category)的作用是在查文章的同时把栏目一起 join 出来模板里访问article.category.name不会再多打一次库。一页 15 条没有这行就是 15 次额外查询列表页慢不慢基本看这个。Paginator.get_page相比page()的好处是页码非法时返回合法页而不是抛 404用户手改 URL 不会看到报错页。build_nav()是我一般会加缓存的函数导航数据一天改不了几次每次请求都查库纯属浪费。模板层保持简单循环、判断、截断摘要别在模板里做复杂计算。有一个容易忽略的点正文是富文本 HTML模板里输出必须标记为安全字符串但前提是后端在写入时已经做过一次清洗把script、onerror这类东西过滤掉。信任前端提交的富文本直接输出是后台类项目里最常见的漏洞来源。4.2 URL 设计slug 拼路径别把主键暴露出去前台 URL 结构一般是/{栏目slug}/{内容slug}.html这种形式栏目和内容各占一段末尾带.html后缀伪装静态页。这套写法对搜索引擎友好用户看着也顺眼。# backend/config/urls_front.py from django.urls import re_path from apps.content import views_front urlpatterns [ # 栏目列表页支持分页 re_path(r^(?Pslug[-\w])/$, views_front.category_list, namecategory), re_path(r^(?Pslug[-\w])/page/(?Ppage\d)/$, views_front.category_list, namecategory_page), # 内容详情页slug 里允许出现字母数字和连字符 re_path(r^(?Pcat_slug[-\w])/(?Pslug[-\w])\.html$, views_front.article_detail, namearticle), ]用re_path而不是path是因为 slug 的匹配规则需要自定义。这里要注意路由顺序详情页规则要放在栏目页之后否则news/这种路径可能被详情页规则先吃掉。page参数用命名分组接住视图函数签名里给个默认值page1不带页码时也能正常渲染第一页。还有一个细节slug 允许中文吗技术上可以URL 编码之后能用但分享出去的链接会变成一长串%E4%B8%AD%E6%96%87。我的习惯是在后台保存时做一个转换英文和数字保留其余字符转拼音或者直接生成时间戳后缀保证 URL 干净。转换逻辑放在模型的save里做或者单独抽一个工具函数在序列化器校验时调用。4.3 静态化与缓存发布之后前台多久能看到内容站的访问特点是读多写少静态化收益很高。这套代码的做法是发布动作触发一次页面重建把渲染结果写到磁盘上的静态目录Nginx 优先读静态文件没有命中再回源到应用。# backend/apps/content/tasks.py import os from django.conf import settings from django.template.loader import render_to_string from django.utils.text import slugify from .models import Article def rebuild_article(article_id): 重建单篇内容的静态页发布、下线、编辑后调用 try: article Article.objects.select_related(category).get(pkarticle_id) except Article.DoesNotExist: return cat article.category rel_path f{cat.slug}/{article.slug}.html abs_path os.path.join(settings.STATIC_SITE_ROOT, rel_path) if article.status ! published: # 下线的内容直接删掉静态文件避免被直接访问到 if os.path.exists(abs_path): os.remove(abs_path) return html render_to_string(detail.html, { article: article, category: cat, seo: { title: article.title, description: article.summary or article.title, }, }, requestNone) os.makedirs(os.path.dirname(abs_path), exist_okTrue) with open(abs_path, w, encodingutf-8) as f: f.write(html)requestNone是关键参数传了 request模板里生成的链接可能带上当前域名不传则走相对路径换域名之后老页面不会失效。STATIC_SITE_ROOT指向 Nginx 的站点根目录写文件时用exist_okTrue建目录避免首次发布栏目时因目录不存在报错。这段逻辑通常是异步执行的发布接口只负责丢任务不阻塞后台响应。任务队列在这里不只是性能优化更重要的是隔离失败静态化写盘失败不应该让发布动作回滚内容已经进库了重试重建即可。内容列表页的静态化更复杂一点因为涉及分页。常见做法是只静态化前 N 页后面的页码回源到动态渲染因为这个区间的访问量已经很低了全量静态化只是浪费磁盘。5. 部署与联调避坑五个我在真实环境里翻过的车本地跑通和线上稳定运行之间隔着一整个部署拓扑。这套 CMS 的组件不少Nginx、应用进程、数据库、缓存、静态目录任何一处配置不对症状都会以「页面打不开」这种极简形式呈现真正的根因藏在日志里。5.1 上线拓扑谁管静态文件谁管接口先把职责分清楚后面排查才有方向。Nginx 在最外层负责三件事把/api和/admin转发给应用进程、直接返回/media和静态站点目录下的文件、把/的请求按「先找静态页找不到回源」的规则处理。应用进程用 Gunicorn 起多个 workerworker 数量一般是 CPU 核数乘 2 加 1别开太多数据库连接池会被打满。静态目录和上传目录必须挂同一个卷给 Nginx 和应用进程否则应用写进去的文件 Nginx 读不到——这是容器化部署里最常见的一类问题。会话和缓存走 Redis不要用应用进程本地内存。多 worker 下本地内存会话会导致「刚登录成功刷新一下又要登录」的玄学现象本质是请求落到了不同 worker 上。5.2 坑一后台传的图片前台全是裂图现象后台附件库能看到图片缩略图前台页面里的图片全是破图右键复制地址在浏览器里打开是 404。原因后台预览走的是接口返回的完整 URL 或者开发代理前台页面里的图片地址是/media/xxx.jpg这种相对路径Nginx 没有为这个前缀配置静态目录请求被转发给应用进程应用进程在DEBUGFalse时不处理静态文件直接 404。解决在 Nginx 配置里加一段把上传目录直接映射出去同时给一个较长的缓存时间location /media/ { alias /data/cms/media/; # 必须和应用的上传目录是同一个挂载点 expires 7d; access_log off; }alias和root的区别要留意alias是替换匹配到的前缀root是拼接。写错的话路径会变成/data/cms/media/media/xxx.jpg症状还是 404但原因完全不同。5.3 坑二内容改了发布前台还是旧的现象后台明明点了发布状态也是「已发布」前台刷新还是老内容等几分钟才变。原因静态文件还在Nginx 优先命中静态页压根没回源。发布动作只写了数据库没触发静态页重建或者重建任务失败了但没人知道。解决两步。一是确认发布接口里确实调了重建任务看任务队列的日志有没有执行记录二是手动删掉对应的静态文件验证——如果删掉后前台立刻变新说明就是静态化没刷新问题定位在任务这一侧。生产环境我会加一个「清空站点静态缓存」的后台按钮运营自己能操作比等开发排查快得多。5.4 坑三后台菜单能看见点进去 403现象用非超级管理员的账号登录左侧菜单正常显示「角色管理」点进去接口返回 403。原因前端菜单过滤用的是登录接口返回的权限码数组后端接口用的是角色表里的perms字段两处数据源不一致。常见情况是改了角色权限但没让用户重新登录登录态里缓存的还是旧数组或者新加的接口挂了权限码但没同步加到角色的默认权限里。解决权限码统一从后端下发前端不做任何本地推导。角色权限变更后强制刷新登录态最简单的方式是让该角色下的用户下次请求时重新拉一次用户信息。新接口上线前过一遍权限清单确认每个require_perm的码都在角色配置页的可选项里。5.5 坑四富文本里的图片存了绝对地址换域名全挂现象站点从测试域名切到正式域名正文里的图片全部裂开附件库里的图片却正常。原因富文本编辑器上传图片时返回的是带域名的绝对地址直接写进了正文 HTML。换域名之后这些地址指向的还是旧域名。解决上传接口一律返回相对路径前端展示时再拼当前域名。已经存了绝对地址的历史数据跑一次批量替换的脚本把https://old-domain.com/media/换成/media/# 一次性数据修复脚本跑之前先备份 from apps.content.models import Article OLD https://old-domain.com/media/ articles Article.objects.filter(body__containsOLD) for a in articles: a.body a.body.replace(OLD, /media/) a.save(update_fields[body, updated_at]) print(ffixed {articles.count()} rows)filter(body__contains...)在大表上是全表扫描几万条以内问题不大量大的话按主键分批跑每批一千条。5.6 坑五列表页翻到后面越来越慢现象第一页秒开翻到第五十页要等好几秒。原因Paginator的 count 查询加上LIMIT offset, size的深分页offset 越大扫过的行越多。加上order_by里带了is_top、sort、published_at三个字段没有对应索引时每次都要排序。解决给(content_type, status, published_at)建联合索引把order_by的字段顺序和索引对齐分页改成基于上一页最后一条记录的游标分页或者限制最大可翻页数——内容站很少有用户真的翻到第五十页限制在 100 页以内基本不影响体验还能挡住爬虫的遍历。6. 二次开发加一个自定义内容类型要动的几处这套代码最值得夸的地方是内容类型用注册表管理加一个「产品」类型不需要改表结构。我把改动位置列成一张表照着走一遍基本不会漏层级位置改什么注册content/content_types.py加一个 key 和字段定义接口api/serializers.py按类型做扩展字段校验后台admin-web/src/views/content/复用列表页按类型切列路由admin-web/src/router/index.js加菜单项和权限码前台templates/新建一套列表和详情模板静态化content/tasks.py确认新类型走同一条重建逻辑注册表的写法大致是这样字段定义用来自动生成后台表单和校验规则# backend/apps/content/content_types.py CONTENT_TYPES { article: { label: 文章, fields: { source: {type: char, label: 来源, max_length: 100}, author_name: {type: char, label: 作者, max_length: 50}, }, }, product: { label: 产品, fields: { price: {type: decimal, label: 价格, required: True}, spec: {type: text, label: 规格参数}, gallery: {type: list, label: 图集}, }, }, }后台的内容编辑表单读这份定义动态渲染前台模板按content_type判断走哪套排版静态化任务对所有类型一视同仁。真正需要单独写的只有模板和序列化器里的特殊校验其余全部复用。验证顺序我的习惯是先接口后界面用 curl 或接口调试工具创建一条产品数据确认extra落库正确再去后台列表页看字段是否正常展示最后访问前台详情页确认模板取到了extra.price这些值发布之后检查静态目录里有没有生成对应的 HTML 文件。四步都过了这个内容类型才算真的加完。从那以后我每加一个内容类型都会先把上面那张表抄在纸上改完一处划掉一处绝不凭记忆判断「应该改完了」。前台刷不出来的时候第一件事不是翻模板而是去静态目录看一眼文件在不在——十次里有八次问题就出在这一步。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑