拓冰建站拓冰建站
首页 / 资讯中心 / 正文

用Django从零构建随机塔罗牌生成器与轻量CMS

简介tarot_juicer是一个基于Django构建的塔罗牌主题CMS示例核心功能是随机生成22张大阿尔卡纳塔罗牌键每张牌展示图片、名称、占星属性、炼金术属性、希伯来字母、字母含义及象征意义等多元信息并尝试将塔罗内容与好莱坞电影、历史传记等非常规场景结合适合学习Django动态页面生成与内容组织方式的开发者。压缩包共281个文件包括95个JPG、27个GIF、18个PNG等图片素材53个Python源码文件25个HTML模板10个CSS样式表以及SVG、JS、字体文件和少量文档整体大小约10.62MB目录结构清晰便于按功能模块阅读。资源已有1957人学习适合具有基础Python/Django知识、希望参考多应用站点结构的开发者。通过解压后可查看登录门户、内容展示、随机牌面生成等功能应用以及数据库备份文件和主题样式设计能直观理解一个小型CMS的页面模板、静态资源与后端逻辑如何协同工作。1. 项目整体设计与思路拆解1.1 为什么叫 tarot_juicer项目名tarot_juicer里的 juicer 其实是我自己随手起的本意是榨汁机——把一堆散落在网页和书籍里的塔罗牌资料榨成结构化数据再用程序生成随机牌面。做这个项目的起因很简单市面上塔罗相关的 App 大多只给结果不给过程牌意解释又写得玄之又玄我想自己用 Django 搭一套能管理内容、能随机抽牌、又能随时修改解释文案的小系统。如果你正在学 Django又不想做那种烂大街的博客项目这个方向很值得一试既有 CMS 的内容管理逻辑又有随机算法和业务状态能一次性覆盖 Python 后端开发的大部分基础知识点。先说清楚这篇文章只聊技术和内容组织不讨论神秘力量。塔罗牌对我来说就是一个内容集合 随机抽取规则的载体真正有意思的是怎么把 78 张牌的数据模型、牌阵逻辑、解释文案管理串起来。哪怕你把牌库换成今日食谱或随机健身动作这套代码骨架依然成立这才是这个项目最有价值的地方。1.2 技术选型为什么选 Django 而不是 Flask认识我的朋友都知道我是个务实派能用现成的就不用自己造。选 Django 作为核心框架理由有三条。第一内置 ORM 和管理后台。塔罗牌有 78 张主牌每张牌有正位解释、逆位解释、关键词、图片、所属牌组再加上牌阵、抽牌记录、CMS 文章这些数据关系如果全手写 SQL前三个晚上就耗在建表上了。Django 的 ORM 让我能用 Python 类描述数据模型一条makemigrations就把表建好admin 后台还能直接用图形界面增删改查省掉一大半精力。第二模板系统足够轻量。这个项目前端的交互形态其实很传统点击抽牌、展示结果、查看文章详情。这些页面不需要前后端分离Django 模板加一点原生 JavaScript 就能跑得很顺。第三生态成熟踩坑成本低。Django 的社区资料非常多遇到问题搜索一下基本都有答案。我常常推荐刚入门的人直接看《Django 5 By Example》这本书里面从建项目到部署讲得非常细比零散看教程高效得多。1.3 项目边界CMS 要做到什么程度标题里的基本 Django CMS是我刻意设置的边界。最初我也纠结过要不要直接装 django-cms 或 wagtail后来想明白一个道理这个项目的核心是随机塔罗牌生成器CMS 只是配套设施用来管理牌意解释、发布相关文章。如果直接上重型 CMS光插件配置、模板结构就能把人绕晕反而冲淡了主线。所以我的方案是用 Django admin 做基础内容管理再为牌意解释和文章内容建立独立模型。这样既能利用 admin 现成的登录、权限、增删改查能力又能让业务模型和界面完全可控。后续如果真想升级成完整 CMS再把模型迁移到 django-cms 也不迟接口可以做到平滑过渡。2. 随机塔罗牌生成器的核心实现2.1 牌库数据建模随机塔罗牌生成器要处理的核心数据有三块牌本身、牌阵、抽牌记录。我建了三个模型Card存放牌面信息、Spread定义牌阵比如三张牌过去/现在/未来、Reading保存每次抽牌的结果。Card模型的字段设计如下from django.db import models class Card(models.Model): name models.CharField(牌名, max_length50, uniqueTrue) arcana models.CharField( 牌组, max_length10, choices[(major, 大阿卡那), (minor, 小阿卡那)], ) suit models.CharField( 花色, max_length20, blankTrue, choices[(wands, 权杖), (cups, 圣杯), (swords, 宝剑), (pentacles, 星币)], ) number models.PositiveSmallIntegerField(序号, nullTrue, blankTrue) image models.ImageField(牌面图片, upload_tocards/, blankTrue) keywords models.CharField(关键词, max_length200, blankTrue) upright_meaning models.TextField(正位含义) reversed_meaning models.TextField(逆位含义) def __str__(self): return self.name这里有几个细节值得注意。uniqueTrue保证牌名不重复避免后面随机抽牌时出现重复数据suit和arcana用choices而不是自由字符串是为了后台下拉选择方便也不容易写错number字段用来支持小阿卡那牌的排序比如权杖一、权杖二这种顺序。Reading模型则是记录一次完整的抽牌行为包含抽取时间、使用的牌阵、以及每张牌落在哪个位置。因为一次抽牌可能对应多张牌所以用了中间模型ReadingCard来维护多对多关系class Reading(models.Model): spread models.ForeignKey(Spread, on_deletemodels.PROTECT) created_at models.DateTimeField(auto_now_addTrue) class ReadingCard(models.Model): reading models.ForeignKey(Reading, related_namecards, on_deletemodels.CASCADE) card models.ForeignKey(Card, on_deletemodels.PROTECT) position models.PositiveSmallIntegerField(位置序号) reversed models.BooleanField(是否逆位, defaultFalse)on_deletemodels.PROTECT的意思是如果一张牌被抽牌记录引用就不允许直接删掉这条牌数据。这是我从实际项目里学到的经验——很多新手直接默认CASCADE结果删牌的时候连带把历史记录也删光了查数据时一脸懵。2.2 洗牌算法Fisher-Yates 与切牌模拟塔罗抽牌和普通随机选数的区别在于仪式感和不重复。如果只是random.choice(cards)抽到的牌可能会和上一次完全一样用户会觉得程序有 bug。所以我在生成器里实现了两个关键点洗牌和不放回抽取。洗牌用经典的 Fisher-Yates 算法。虽然 Python 有现成的random.shuffle但手写一遍能更清楚它为什么能保证均匀分布import random def shuffle_deck(cards): deck list(cards) for i in range(len(deck) - 1, 0, -1): j random.randint(0, i) deck[i], deck[j] deck[j], deck[i] return deck这个算法的核心思想是从后往前遍历每次都把当前位置的元素和前面任意一个随机位置的元素交换。每个位置被选中的概率都是等概率的所以洗牌结果不会偏向某张牌。实际跑下来 78 张牌洗一万次每张牌出现在任意位置的概率都很接近 1/78分布很均匀。切牌模拟则是另一个有意思的小细节洗完牌之后从前 20 张到后 20 张之间随机取一个切牌点重新拼接def cut_deck(deck): if len(deck) 2: return deck cut_index random.randint(max(1, len(deck) // 4), min(len(deck) - 1, len(deck) * 3 // 4)) return deck[cut_index:] deck[:cut_index]切牌点不从正中间取而是限制在偏前或偏后的范围内是为了更贴近现实里随手切一下的感觉。它不会改变统计分布但能让抽牌过程多一点变化用户也更愿意接受结果。2.3 抽牌逻辑与牌阵支持洗好牌后抽牌其实就是从牌堆顶部按顺序取牌。这样能保证同一套牌阵里的牌绝不重复因为牌堆里的 78 张牌只会被取走一次def draw_cards(spread_code, card_queryset): deck shuffle_deck(card_queryset) deck cut_deck(deck) spread Spread.objects.get(codespread_code) positions list(spread.positions.order_by(order)) if len(positions) len(deck): raise ValueError(牌阵位置数超过牌库数量) result [] for i, pos in enumerate(positions): card deck[i] reversed_flag random.random() 0.3 # 三成概率逆位 result.append({ position_name: pos.name, card: card, reversed: reversed_flag, meaning: card.reversed_meaning if reversed_flag else card.upright_meaning, }) return result逆位概率设置成 30% 是我个人的偏好经典做法也有五五开或者完全由用户洗牌时决定。这个值放在常量里想调随时能调。关键是先洗牌再取牌这个顺序不能反如果每张都单独random.choice就会出现重复牌懂的都懂。3. Django CMS 内容管理集成3.1 自研轻量 CMS 还是直接上 django-cms我在 1.3 里说清楚了边界这里补充一下决策背后的对比逻辑。直接上 django-cms 的优势很明确插件系统、页面树、多语言支持都是现成的适合做真正的内容门户。但代价也很大——它的模板结构有自己的约定新手照着文档搭一遍至少得两三天而且大部分功能在你的场景里根本用不到。自研轻量 CMS 的思路则是把文章和牌意解释当成两个普通 Django 模型用 admin 统一管理再用模板渲染出来。这样所有代码都是自己可控的出了 bug 一眼就知道在哪改。经过实际对比我这个项目的体量下自研方案明显更省时后期想加功能也更容易。我最后实现了一套极简内容模型class Article(models.Model): title models.CharField(标题, max_length200) slug models.SlugField(URL别名, uniqueTrue) category models.CharField( 分类, max_length20, choices[(guide, 入门指南), (story, 牌意故事), (update, 更新日志)], ) content models.TextField(正文) created_at models.DateTimeField(auto_now_addTrue) published models.BooleanField(已发布, defaultTrue)这套模型覆盖了 CMS 最基础的能力发布状态、分类、URL 别名、正文内容。配合 admin 的列表页筛选和搜索基本够用。3.2 后台管理与富文本编辑Django admin 自带的文本框写纯文本体验很差所以我在后台给Article.content接了一个富文本编辑器。项目里我用的是django-wangeditor它基于 wangEditor集成方式很直接装好之后在 admin 里指定formfield_overrides就行。有一点要特别提醒富文本编辑器会把 HTML 原样写进数据库这会带来 XSS 风险。如果不做任何清洗用户如果能在后台录入内容他就可以在页面里塞任意脚本。我的做法是渲染模板时用safe过滤器前先做一次白名单清洗比如只允许保留常见的段落、链接、加粗标签。千万别因为省事直接{{ article.content|safe }}裸奔上线。其实这也说明了一个问题Django 自带的 admin 不是不能当 CMS只是你得把内容安全当成第一优先级。后台这么强的编辑权限给谁用、能改哪些字段都要在权限配置里想清楚。3.3 前端模板与卡片页面渲染前端这块我用了一套最简单的模板继承结构。base.html放导航和通用样式card_detail.html展示单张牌详情reading_result.html展示抽牌结果article_list.html和article_detail.html管 CMS 文章。拿牌面详情页来说页面上要展示的内容包括牌名、牌组、关键词、正位含义、逆位含义。这些字段在Card模型里已经有了模板里只需要循环输出即可。为了让页面不显得干巴巴我还用原生 JavaScript 实现了一个翻牌动画默认显示卡背点击后翻转显示正面含义。CMS 文章列表页更简单就是按created_at倒序输出已发布文章。关键点是 URL 设计文章详情通过slug访问不通过自增 id。这样 URL 更友好也不容易暴露文章数量。4. 实操过程从零到一跑通全流程4.1 初始化项目与应用这一步我已经做过很多次了流程固定如下# 创建虚拟环境 python -m venv venv source venv/bin/activate # Windows 下用 venv\Scripts\activate # 安装依赖 pip install django pillow django-wangeditor # 创建项目和 app django-admin startproject tarot_juicer cd tarot_juicer python manage.py startapp tarot python manage.py startapp cms我习惯把业务逻辑放tarotapp文章管理放cmsapp两个 app 职责分开后面维护时找代码特别快。然后在settings.py里注册 app并配置静态文件目录和上传文件目录INSTALLED_APPS [ django.contrib.admin, django.contrib.auth, django.contrib.contenttypes, django.contrib.sessions, django.contrib.messages, django.contrib.staticfiles, tarot, cms, django_wangeditor, ] MEDIA_URL /media/ MEDIA_ROOT BASE_DIR / media4.2 核心代码实现视图函数我统一放在tarot/views.py里。抽牌接口用 POST 请求触发页面刷新后展示结果from django.shortcuts import render, get_object_or_404 from .models import Card, Spread, Reading from .utils import draw_cards def home(request): spreads Spread.objects.all() return render(request, tarot/home.html, {spreads: spreads}) def reading_result(request, spread_code): spread get_object_or_404(Spread, codespread_code) if request.method POST: result draw_cards(spread_code, Card.objects.all()) reading Reading.objects.create(spreadspread) for item in result: reading.cards.create( carditem[card], positionitem[position_name], reverseditem[reversed], ) return render(request, tarot/reading_result.html, {result: result}) return redirect(home)URL 配置在tarot_juicer/urls.py下进行后台和媒体文件的 URL 一起挂上from django.conf import settings from django.conf.urls.static import static from django.contrib import admin from django.urls import path, include urlpatterns [ path(admin/, admin.site.urls), path(, include(tarot.urls)), path(cms/, include(cms.urls)), ] if settings.DEBUG: urlpatterns static(settings.MEDIA_URL, document_rootsettings.MEDIA_ROOT)4.3 数据迁移与启用后台模型写好后依次执行python manage.py makemigrations python manage.py migrate python manage.py createsuperuser python manage.py runserver这一步我踩过最大的坑是修改模型字段后忘记重新 makemigrations导致数据库和模型不一致。尤其是给既有字段加了uniqueTrue之后如果表里已经有重复数据migrate 会直接报错。所以每次改模型我都会先检查现有数据有没有违反约束。admin 注册代码写得很简单from django.contrib import admin from .models import Card, Spread, Reading, ReadingCard admin.register(Card) class CardAdmin(admin.ModelAdmin): list_display (name, arcana, suit, number) search_fields (name, keywords) list_filter (arcana, suit)注册完之后后台就能直接新增 78 张牌的数据。我当时是写了个脚本批量导入从 JSON 文件里读出所有牌的数据再循环创建。如果你手头没有现成数据也可以先在 admin 里手动录入几张测试牌先把流程跑通。5. 常见问题与排查技巧实录5.1 随机结果不随机或者连续两次抽到同一张牌这是一个高频问题。原因多半是每次请求都重新调用了random.seed()或者把随机种子设成了固定值。在生产逻辑里不要在代码里手动设置随机种子让 Python 用系统熵初始化。真正要做到不重复必须用 2.2 里的洗牌后从顶部抽取思路而不是每次独立抽取。另外有些人用random.sample(cards, 3)来抽三张牌这也能避免重复但它不会有切牌的过程。实际体验下来加了切牌模拟之后用户反馈更好因为结果更像人工操作亲和力强很多。5.2 静态文件和上传图片不显示开发阶段常见的报错有 404 找不到图片或者/media/cards/xxx.jpg返回空。原因多半是两个第一MEDIA_URL和MEDIA_ROOT没配好第二开发环境没有在urlpatterns里挂载媒体文件路由。我是在tarot_juicer/urls.py里用if settings.DEBUG判断后挂载的。生产环境则要交给 Nginx 或对象存储处理这个在部署时再配置。5.3 CMS 内容与业务数据耦合我一开始把牌意解释直接写死在Card.upright_meaning里CMS 文章又放在另一个 app。后面发现一个尴尬的问题想在解释里补充一段新手如何理解这张牌的文章链接只能硬塞进 TextField 里。后来我调整了逻辑给Card加了一个可空的related_article外键关联到cms.Article这样后台编辑牌面时可以直接选一篇关联文章模板里自动渲染链接。这种内容之间互相引用的需求在建模时最好提前留个外键位不然后期加字段很痛苦。5.4 批量删除和查询的坑用 Django 执行查询和删除对象其实有不少需要注意的地方。比如我想清理超过 30 天的抽牌记录新手可能这么写for reading in Reading.objects.filter(created_at__ltcutoff): reading.delete()这样写不是不行但每循环一次就执行一条 DELETE SQL数据量大了效率很低。正确做法是用 QuerySet 的批量删除Reading.objects.filter(created_at__ltcutoff).delete()会一次性删除所有匹配记录而且由于ReadingCard使用了on_deletemodels.CASCADE关联的子记录也会自动清掉。另一个坑是如果对 QuerySet 做了切片或迭代再调用.delete()Django 会明确报错。原因是不允许对已求值的 QuerySet 批量删除防止误操作。这个细节在 Django 相关面试题里也经常被问到建议记住。5.5 部署与安全部署阶段我用的是 Gunicorn Nginx 的标准组合这里只说两个容易忽略的安全点。第一后台的登录接口一定要限流admin 路径建议换个不常见的名字不要裸用/admin/。虽然这不能防住所有攻击但能挡掉大部分扫描脚本。第二用户上传的图片必须做类型校验不能只靠forms.ImageField扩展名判断必要时用 Pillow 验证图片二进制内容。CMS 系统最怕的就是上传伪造图片绕过安全检查这一点我在之前的文章里反复强调过。如果你做的版本要对外提供 SEO 功能还可以为每张牌生成独立的详情页然后在模板里写meta description和og:title。这一步既提升搜索引擎收录质量又让用户分享链接时展示更精致。最后再分享一个经验我第一次跑通抽牌功能的时候客户端只能看到一行文字你抽到了愚者正位。说实话那一刻非常没有成就感。后来我花了一个晚上把卡面图片、逆位翻转、牌阵位置说明都补齐页面瞬间有了生命力。这个项目给我最大的启发是技术逻辑只是底座用户能感知到的永远是内容和交互。CMS 的意义就在这里——它让一个没有编程背景的人也能修改牌意解释、上传新图、发布文章整个系统才真正活起来。你现在如果也在做一个类似的 Django 练手项目建议从最核心的一条业务链路出发先把抽牌 展示结果跑通再逐步加 CMS、用户系统、统计报表。别一开始就想着做完美产品先把 78 张牌的数据和基础逻辑弄干净后面每一步都会很顺。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门