chore: initial commit of Wagtail backend scaffold

This commit is contained in:
2026-08-06 15:56:14 +08:00
commit 7f51a31150
51 changed files with 1239 additions and 0 deletions
View File
+11
View File
@@ -0,0 +1,11 @@
from django.apps import AppConfig
class CoreConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "apps.core"
label = "core"
verbose_name = "核心 / 公共基础"
def ready(self):
from apps.core import signals # noqa: F401
+108
View File
@@ -0,0 +1,108 @@
"""
可复用的 StreamField Block 库。
参见 documents/设计方案分析与完善版.md §2.6 组件清单。
"""
from django.utils.translation import gettext_lazy as _
from wagtail import blocks
from wagtail.images.blocks import ImageChooserBlock
class CTAButtonBlock(blocks.StructBlock):
text = blocks.CharBlock(max_length=50, label=_("按钮文案"))
link = blocks.URLBlock(label=_("按钮链接"))
class Meta:
icon = "link"
label = _("按钮")
class HeroBlock(blocks.StructBlock):
title = blocks.CharBlock(max_length=120, label=_("标题"))
subtitle = blocks.TextBlock(required=False, label=_("副标题"))
background_image = ImageChooserBlock(required=False, label=_("背景图"))
buttons = blocks.ListBlock(CTAButtonBlock(), label=_("按钮组"))
class Meta:
icon = "image"
label = _("首屏 Hero")
template = "core/blocks/hero_block.html"
class StatItemBlock(blocks.StructBlock):
value = blocks.CharBlock(max_length=20, label=_("数值"))
label = blocks.CharBlock(max_length=50, label=_("说明文字"))
class StatsBlock(blocks.StructBlock):
items = blocks.ListBlock(StatItemBlock(), label=_("数据项"))
class Meta:
icon = "site"
label = _("数据展示")
template = "core/blocks/stats_block.html"
class FeatureBlock(blocks.StructBlock):
icon = blocks.CharBlock(max_length=50, required=False, label=_("图标"))
title = blocks.CharBlock(max_length=100, label=_("标题"))
description = blocks.TextBlock(label=_("描述"))
class Meta:
icon = "pick"
label = _("能力/优势")
template = "core/blocks/feature_block.html"
class FeatureGridBlock(blocks.StructBlock):
heading = blocks.CharBlock(max_length=100, required=False, label=_("模块标题"))
items = blocks.ListBlock(FeatureBlock(), label=_("能力列表"))
class Meta:
icon = "grip"
label = _("能力网格")
class FAQItemBlock(blocks.StructBlock):
question = blocks.CharBlock(max_length=200, label=_("问题"))
answer = blocks.RichTextBlock(label=_("回答"))
class FAQBlock(blocks.StructBlock):
items = blocks.ListBlock(FAQItemBlock(), label=_("问答列表"))
class Meta:
icon = "help"
label = _("常见问题")
template = "core/blocks/faq_block.html"
class CTABlock(blocks.StructBlock):
heading = blocks.CharBlock(max_length=120, label=_("标题"))
description = blocks.TextBlock(required=False, label=_("描述"))
button = CTAButtonBlock(label=_("按钮"))
class Meta:
icon = "plus"
label = _("行动号召 CTA")
template = "core/blocks/cta_block.html"
class LogoCloudBlock(blocks.StructBlock):
heading = blocks.CharBlock(max_length=100, required=False, label=_("标题"))
logos = blocks.ListBlock(ImageChooserBlock(), label=_("Logo 列表"))
class Meta:
icon = "image"
label = _("客户 Logo 墙")
template = "core/blocks/logo_cloud_block.html"
COMMON_BLOCKS = [
("hero", HeroBlock()),
("stats", StatsBlock()),
("feature_grid", FeatureGridBlock()),
("faq", FAQBlock()),
("cta", CTABlock()),
("logo_cloud", LogoCloudBlock()),
("richtext", blocks.RichTextBlock(label=_("富文本"))),
]
View File
+38
View File
@@ -0,0 +1,38 @@
"""
公共抽象模型。所有业务页面应继承 SEOablePage 而非直接继承 wagtail.models.Page
以统一获得 SEO 字段(详见 documents/设计方案分析与完善版.md §2.5)。
"""
from django.db import models
from wagtail.admin.panels import FieldPanel
from wagtail.models import Page
class SEOablePage(Page):
seo_title_override = models.CharField(
max_length=70, blank=True, verbose_name="SEO 标题",
help_text="留空则使用页面标题",
)
seo_description_override = models.CharField(
max_length=160, blank=True, verbose_name="SEO 描述",
)
og_image = models.ForeignKey(
"wagtailimages.Image",
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="+",
verbose_name="社交分享图",
)
canonical_url = models.URLField(blank=True, verbose_name="Canonical URL")
schema_json = models.JSONField(blank=True, default=dict, verbose_name="结构化数据 (JSON-LD)")
promote_panels = Page.promote_panels + [
FieldPanel("seo_title_override"),
FieldPanel("seo_description_override"),
FieldPanel("og_image"),
FieldPanel("canonical_url"),
FieldPanel("schema_json"),
]
class Meta:
abstract = True
+27
View File
@@ -0,0 +1,27 @@
"""
Wagtail signal handlers:发布内容后通知 Next.js 前端失效 ISR 缓存。
详见 documents/设计方案分析与完善版.md §2.7。
"""
import logging
import requests
from django.conf import settings
from django.dispatch import receiver
from wagtail.signals import page_published
logger = logging.getLogger(__name__)
@receiver(page_published)
def notify_frontend_revalidate(sender, instance, **kwargs):
if not settings.FRONTEND_REVALIDATE_URL:
return
try:
requests.post(
settings.FRONTEND_REVALIDATE_URL,
json={"tags": [f"page:{instance.id}", instance.url_path]},
headers={"Authorization": f"Bearer {settings.REVALIDATE_SECRET}"},
timeout=3,
)
except requests.RequestException:
logger.warning("Failed to notify frontend revalidate webhook", exc_info=True)
@@ -0,0 +1,5 @@
<section class="cta">
<h2>{{ value.heading }}</h2>
{% if value.description %}<p>{{ value.description }}</p>{% endif %}
<a href="{{ value.button.link }}">{{ value.button.text }}</a>
</section>
@@ -0,0 +1,9 @@
{% load wagtailcore_tags %}
<section class="faq">
{% for item in value.items %}
<div class="faq-item">
<h4>{{ item.question }}</h4>
<div>{{ item.answer|richtext }}</div>
</div>
{% endfor %}
</section>
@@ -0,0 +1,4 @@
<section class="feature">
<h3>{{ value.title }}</h3>
<p>{{ value.description }}</p>
</section>
@@ -0,0 +1,8 @@
{% load wagtailcore_tags %}
<section class="hero">
<h1>{{ value.title }}</h1>
{% if value.subtitle %}<p>{{ value.subtitle }}</p>{% endif %}
{% for button in value.buttons %}
<a href="{{ button.link }}">{{ button.text }}</a>
{% endfor %}
</section>
@@ -0,0 +1,10 @@
{% load wagtailimages_tags %}
<section class="logo-cloud">
{% if value.heading %}<h3>{{ value.heading }}</h3>{% endif %}
<div class="logos">
{% for logo in value.logos %}
{% image logo width-160 as logo_img %}
<img src="{{ logo_img.url }}" alt="">
{% endfor %}
</div>
</section>
@@ -0,0 +1,5 @@
<section class="stats">
{% for item in value.items %}
<div class="stat"><strong>{{ item.value }}</strong><span>{{ item.label }}</span></div>
{% endfor %}
</section>
+21
View File
@@ -0,0 +1,21 @@
"""
健康检查端点,供 Kubernetes liveness/readiness 探针使用。
详见 documents/设计方案分析与完善版.md §2.12。
"""
from django.db import connection
from django.http import JsonResponse
def healthz(request):
"""存活探针:仅确认进程可响应请求。"""
return JsonResponse({"status": "ok"})
def readyz(request):
"""就绪探针:确认数据库连接可用。"""
try:
with connection.cursor() as cursor:
cursor.execute("SELECT 1")
except Exception as exc: # pragma: no cover - defensive
return JsonResponse({"status": "error", "detail": str(exc)}, status=503)
return JsonResponse({"status": "ok"})