Initial commit: hulai admin API backend

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
这个提交包含在:
刘涛
2026-03-22 22:01:23 +08:00
共同撰写人 Claude Sonnet 4.6
当前提交 8c0a5f489d
共修改 114 个文件,包含 7619 行新增和 0 行删除
+28
查看文件
@@ -0,0 +1,28 @@
from app.models.user import AdminUser
from app.models.brand import Brand, BrandTrustStat, BrandDifferentiator
from app.models.product import ProductConfig, ProductVersion, ProductVersionHighlight, SummerCamp, SummerCampPrinciple, SummerCampActivity, SummerCampItinerary, SummerCampFaq, SelectionGuide
from app.models.faq import FaqCategory, FaqQuestion
from app.models.review import ReviewSummary, Review
from app.models.about import AboutStory, AboutSubsidiary, AboutCertification, AboutTrademark, AboutCopyright, AboutGuarantee, AboutXiaohongshu, AboutTeam, AboutCulture, AboutCultureValue
from app.models.contact import ContactChannel, ContactConfig
from app.models.seo import SeoPage
from app.models.navigation import NavHeader, NavFooterGroup, NavFooterLink
from app.models.version import VersionConfig, VersionUpgrade, VersionHighlight, VersionCompare, VersionTimeline, VersionTimelineChange, VersionPhilosophy
from app.models.guide import GuideConfig, GuideSection
from app.models.site_image import SiteImage
from app.models.export_log import ExportLog
from app.models.seasonal_product import SeasonalProductConfig, SeasonalProductVersion, SeasonalProductHighlight, SeasonalProductTimeline
from app.models.winter_camp import WinterCampConfig, WinterCampHotel, WinterCampItinerary, WinterCampFaq
from app.models.destination import DestinationConfig, DestinationItem, DestinationDimension, DestinationHonestItem
from app.models.blog import Blog
from app.models.story import Story
from app.models.news import News
from app.models.pricing import PricingItem
from app.models.qualification import Qualification
from app.models.partner import Partner
from app.models.calendar import CalendarMonth
from app.models.destination_detail import DestinationDetail
from app.models.gallery import GalleryItem
from app.models.courses import CoursesConfig
from app.models.selector import SelectorConfig
from app.models.customize_submission import CustomizeSubmission
+206
查看文件
@@ -0,0 +1,206 @@
from __future__ import annotations
from sqlalchemy import Column, Integer, String, Text, Boolean, DateTime, ForeignKey, JSON
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from app.database import Base
class AboutStory(Base):
__tablename__ = "about_story"
id = Column(Integer, primary_key=True, autoincrement=True)
title = Column(String(200))
content = Column(Text)
founding_moment = Column(String(500))
totem_description = Column(Text)
totem_tagline = Column(String(500))
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
class AboutSubsidiary(Base):
__tablename__ = "about_subsidiaries"
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String(255), nullable=False)
role = Column(Text)
established = Column(String(20))
sort_order = Column(Integer, default=0)
class AboutCertification(Base):
__tablename__ = "about_certifications"
id = Column(Integer, primary_key=True, autoincrement=True)
title = Column(String(255), nullable=False)
detail = Column(String(500))
sort_order = Column(Integer, default=0)
class AboutTrademark(Base):
__tablename__ = "about_trademark"
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String(100))
scope = Column(String(100))
holder = Column(String(255))
description = Column(Text)
class AboutCopyright(Base):
__tablename__ = "about_copyrights"
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String(200), nullable=False)
reg_no = Column(String(100))
category = Column(String(100))
holder = Column(String(255))
date = Column(String(20))
description = Column(Text)
sort_order = Column(Integer, default=0)
class AboutGuarantee(Base):
__tablename__ = "about_guarantees"
id = Column(Integer, primary_key=True, autoincrement=True)
title = Column(String(255), nullable=False)
detail = Column(Text)
sort_order = Column(Integer, default=0)
class AboutXiaohongshu(Base):
__tablename__ = "about_xiaohongshu"
id = Column(Integer, primary_key=True, autoincrement=True)
account = Column(String(200))
verified = Column(Boolean, default=False)
verified_type = Column(String(200))
followers = Column(String(50))
likes = Column(String(50))
awards = Column(JSON)
tagline = Column(String(500))
tags = Column(JSON)
description = Column(Text)
class AboutTeam(Base):
__tablename__ = "about_team"
id = Column(Integer, primary_key=True, autoincrement=True)
summary = Column(Text)
class AboutCulture(Base):
__tablename__ = "about_culture"
id = Column(Integer, primary_key=True, autoincrement=True)
core = Column(String(200))
transparency = Column(Text)
trust = Column(String(500))
grief_award_name = Column(String(100))
grief_award_description = Column(Text)
values = relationship("AboutCultureValue", back_populates="culture", cascade="all, delete-orphan", order_by="AboutCultureValue.sort_order")
class AboutCultureValue(Base):
__tablename__ = "about_culture_values"
id = Column(Integer, primary_key=True, autoincrement=True)
culture_id = Column(Integer, ForeignKey("about_culture.id", ondelete="CASCADE"), nullable=False)
name = Column(String(50), nullable=False)
expression = Column(Text)
sort_order = Column(Integer, default=0)
culture = relationship("AboutCulture", back_populates="values")
class AboutFounderDetail(Base):
"""单个创始人详细介绍(刘涛)"""
__tablename__ = "about_founder_detail"
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String(100))
title = Column(String(200))
brand_founded = Column(Integer)
years_in_hulunbuir = Column(String(50))
background = Column(Text)
expertise = Column(JSON) # list of strings
media_presence = Column(JSON) # list of strings
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
class AboutFounder(Base):
"""三位创始人简短介绍(列表)"""
__tablename__ = "about_founders"
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String(100), nullable=False)
title = Column(String(200))
story = Column(Text)
sort_order = Column(Integer, default=0)
class AboutMilestone(Base):
"""品牌成长纪事"""
__tablename__ = "about_milestones"
id = Column(Integer, primary_key=True, autoincrement=True)
year = Column(Integer, nullable=False)
event = Column(Text, nullable=False)
sort_order = Column(Integer, default=0)
class AboutServicePrinciple(Base):
"""服务原则(列表字符串)"""
__tablename__ = "about_service_principles"
id = Column(Integer, primary_key=True, autoincrement=True)
text = Column(Text, nullable=False)
sort_order = Column(Integer, default=0)
class AboutDifferentiation(Base):
"""差异化竞争力"""
__tablename__ = "about_differentiation"
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String(100), nullable=False)
detail = Column(Text)
sort_order = Column(Integer, default=0)
class AboutServiceJourney(Base):
"""服务旅程配置(标题+子标题)"""
__tablename__ = "about_service_journey"
id = Column(Integer, primary_key=True, autoincrement=True)
title = Column(String(200))
subtitle = Column(String(500))
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
moments = relationship("AboutServiceMoment", back_populates="journey", cascade="all, delete-orphan", order_by="AboutServiceMoment.sort_order")
class AboutServiceMoment(Base):
"""服务触点(8个时刻)"""
__tablename__ = "about_service_moments"
id = Column(Integer, primary_key=True, autoincrement=True)
journey_id = Column(Integer, ForeignKey("about_service_journey.id", ondelete="CASCADE"), nullable=False)
step = Column(Integer, nullable=False)
name = Column(String(100), nullable=False)
detail = Column(Text)
sort_order = Column(Integer, default=0)
journey = relationship("AboutServiceJourney", back_populates="moments")
class AboutStats(Base):
"""品牌统计数据(单行JSON)"""
__tablename__ = "about_stats"
id = Column(Integer, primary_key=True, autoincrement=True)
data = Column(JSON) # flat key-value dict
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
+24
查看文件
@@ -0,0 +1,24 @@
from __future__ import annotations
from sqlalchemy import Column, Integer, String, Text, Boolean, DateTime, JSON
from sqlalchemy.sql import func
from app.database import Base
class Blog(Base):
__tablename__ = "blog_posts"
id = Column(Integer, primary_key=True, autoincrement=True)
title = Column(String(200), nullable=False)
slug = Column(String(200), unique=True, nullable=False)
cover_image = Column(String(500))
summary = Column(Text)
content = Column(Text)
author = Column(String(100))
category = Column(String(100))
tags = Column(JSON)
published_at = Column(String(50))
is_visible = Column(Boolean, default=True)
sort_order = Column(Integer, default=0)
created_at = Column(DateTime, server_default=func.now())
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
+53
查看文件
@@ -0,0 +1,53 @@
from __future__ import annotations
from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, JSON
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from app.database import Base
class Brand(Base):
__tablename__ = "brand"
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String(100), nullable=False)
full_name = Column(String(255))
domain = Column(String(100))
url = Column(String(255))
slogan_emotional = Column(String(255))
slogan_functional = Column(String(255))
icp_entity = Column(String(255))
icp = Column(String(100))
icp_url = Column(String(255))
e_contract = Column(String(255))
cta_buttons = Column(JSON) # {primary, secondary, pricing, customize, contact}
conversion_path = Column(JSON) # list of {step, label, to}
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
trust_stats = relationship("BrandTrustStat", back_populates="brand", cascade="all, delete-orphan", order_by="BrandTrustStat.sort_order")
differentiators = relationship("BrandDifferentiator", back_populates="brand", cascade="all, delete-orphan", order_by="BrandDifferentiator.sort_order")
class BrandTrustStat(Base):
__tablename__ = "brand_trust_stats"
id = Column(Integer, primary_key=True, autoincrement=True)
brand_id = Column(Integer, ForeignKey("brand.id", ondelete="CASCADE"), nullable=False)
value = Column(String(50), nullable=False)
unit = Column(String(20))
label = Column(String(100), nullable=False)
sort_order = Column(Integer, default=0)
brand = relationship("Brand", back_populates="trust_stats")
class BrandDifferentiator(Base):
__tablename__ = "brand_differentiators"
id = Column(Integer, primary_key=True, autoincrement=True)
brand_id = Column(Integer, ForeignKey("brand.id", ondelete="CASCADE"), nullable=False)
title = Column(String(100), nullable=False)
description = Column(Text)
sort_order = Column(Integer, default=0)
brand = relationship("Brand", back_populates="differentiators")
+21
查看文件
@@ -0,0 +1,21 @@
from __future__ import annotations
from sqlalchemy import Column, Integer, String, Text, Boolean, DateTime, JSON, UniqueConstraint
from sqlalchemy.sql import func
from app.database import Base
class CalendarMonth(Base):
__tablename__ = "calendar_months"
__table_args__ = (UniqueConstraint("year", "month", name="uq_calendar_year_month"),)
id = Column(Integer, primary_key=True, autoincrement=True)
year = Column(Integer, nullable=False)
month = Column(Integer, nullable=False) # 1-12
title = Column(String(100))
description = Column(Text)
weather = Column(String(200))
highlights = Column(JSON) # list of strings
events = Column(JSON) # list of {date, title, description}
is_available = Column(Boolean, default=True)
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
+24
查看文件
@@ -0,0 +1,24 @@
from __future__ import annotations
from sqlalchemy import Column, Integer, String, Text, Boolean
from app.database import Base
class ContactChannel(Base):
__tablename__ = "contact_channels"
id = Column(Integer, primary_key=True, autoincrement=True)
type = Column(String(50), nullable=False)
label = Column(String(100), nullable=False)
value = Column(String(500))
qr_image = Column(String(500))
is_primary = Column(Boolean, default=False)
description = Column(Text)
sort_order = Column(Integer, default=0)
class ContactConfig(Base):
__tablename__ = "contact_config"
id = Column(Integer, primary_key=True, autoincrement=True)
security_notice = Column(Text)
+15
查看文件
@@ -0,0 +1,15 @@
from __future__ import annotations
from sqlalchemy import Column, Integer, JSON, DateTime
from sqlalchemy.sql import func
from app.database import Base
class CoursesConfig(Base):
__tablename__ = "courses_config"
id = Column(Integer, primary_key=True, autoincrement=True)
modules = Column(JSON) # list of course modules
age_groups = Column(JSON) # list of age group configs
faqs = Column(JSON) # list of {question, answer}
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
+19
查看文件
@@ -0,0 +1,19 @@
from __future__ import annotations
from sqlalchemy import Column, Integer, JSON, DateTime
from sqlalchemy.sql import func
from app.database import Base
class CustomizeConfig(Base):
"""定制表单页面配置(选项列表、流程步骤、联系方式等)"""
__tablename__ = "customize_config"
id = Column(Integer, primary_key=True, autoincrement=True)
trust_stats = Column(JSON) # [{value, unit, label}]
durations = Column(JSON) # [{value, label}]
activities = Column(JSON) # [{value, label}]
budgets = Column(JSON) # [{value, label}]
process = Column(JSON) # [{title, desc}]
contact = Column(JSON) # {wechat, tip}
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
+24
查看文件
@@ -0,0 +1,24 @@
from __future__ import annotations
from sqlalchemy import Column, Integer, String, Text, DateTime, JSON, Enum
from sqlalchemy.sql import func
from app.database import Base
class CustomizeSubmission(Base):
__tablename__ = "customize_submissions"
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String(100))
phone = Column(String(50))
wechat = Column(String(100))
adults = Column(Integer, default=0)
children = Column(Integer, default=0)
travel_dates = Column(String(200))
budget = Column(String(100))
interests = Column(JSON) # list of interest tags
notes = Column(Text)
source = Column(String(100)) # 来源页面
status = Column(Enum("pending", "processed"), default="pending")
created_at = Column(DateTime, server_default=func.now())
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
+50
查看文件
@@ -0,0 +1,50 @@
"""Destination comparison (目的地对比) data model."""
from __future__ import annotations
from sqlalchemy import Column, Integer, String, Text, Boolean, DateTime, JSON
from sqlalchemy.sql import func
from app.database import Base
class DestinationConfig(Base):
__tablename__ = "destination_config"
id = Column(Integer, primary_key=True, autoincrement=True)
title = Column(String(200))
subtitle = Column(String(200))
intro = Column(Text)
closing_title = Column(String(200))
closing_text = Column(Text)
data_sources = Column(Text)
honest_title = Column(String(200))
honest_subtitle = Column(String(200))
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
class DestinationItem(Base):
__tablename__ = "destination_items"
id = Column(Integer, primary_key=True, autoincrement=True)
dest_id = Column(String(50), nullable=False)
name = Column(String(100), nullable=False)
tag = Column(String(200))
highlight = Column(Boolean, default=False)
sort_order = Column(Integer, default=0)
class DestinationDimension(Base):
__tablename__ = "destination_dimensions"
id = Column(Integer, primary_key=True, autoincrement=True)
label = Column(String(100), nullable=False)
icon = Column(String(10))
values = Column(JSON) # list of 3 strings, one per destination
sort_order = Column(Integer, default=0)
class DestinationHonestItem(Base):
__tablename__ = "destination_honest_items"
id = Column(Integer, primary_key=True, autoincrement=True)
text = Column(Text, nullable=False)
sort_order = Column(Integer, default=0)
+26
查看文件
@@ -0,0 +1,26 @@
from __future__ import annotations
from sqlalchemy import Column, Integer, String, Text, Boolean, DateTime, JSON
from sqlalchemy.sql import func
from app.database import Base
class DestinationDetail(Base):
__tablename__ = "destination_details"
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String(200), nullable=False)
slug = Column(String(200), unique=True, nullable=False)
subtitle = Column(String(500))
cover_image = Column(String(500))
description = Column(Text)
location = Column(String(200))
best_season = Column(String(200))
duration = Column(String(100))
highlights = Column(JSON) # list of {icon, title, description}
gallery = Column(JSON) # list of image URLs
tags = Column(JSON) # list of strings
is_visible = Column(Boolean, default=True)
sort_order = Column(Integer, default=0)
created_at = Column(DateTime, server_default=func.now())
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
+16
查看文件
@@ -0,0 +1,16 @@
from __future__ import annotations
from sqlalchemy import Column, Integer, String, Text, DateTime, JSON, Enum
from sqlalchemy.sql import func
from app.database import Base
class ExportLog(Base):
__tablename__ = "export_log"
id = Column(Integer, primary_key=True, autoincrement=True)
exported_by = Column(String(50))
modules = Column(JSON)
status = Column(Enum("success", "failed"), default="success")
message = Column(Text)
created_at = Column(DateTime, server_default=func.now())
+30
查看文件
@@ -0,0 +1,30 @@
from __future__ import annotations
from sqlalchemy import Column, Integer, String, Text, ForeignKey, JSON
from sqlalchemy.orm import relationship
from app.database import Base
class FaqCategory(Base):
__tablename__ = "faq_categories"
id = Column(Integer, primary_key=True, autoincrement=True)
category_id = Column(String(50), unique=True, nullable=False)
name = Column(String(100), nullable=False)
sort_order = Column(Integer, default=0)
questions = relationship("FaqQuestion", back_populates="category", cascade="all, delete-orphan", order_by="FaqQuestion.sort_order")
class FaqQuestion(Base):
__tablename__ = "faq_questions"
id = Column(Integer, primary_key=True, autoincrement=True)
category_id = Column(Integer, ForeignKey("faq_categories.id", ondelete="CASCADE"), nullable=False)
question_id = Column(String(50), unique=True, nullable=False)
question = Column(Text, nullable=False)
answer = Column(Text, nullable=False)
related_links = Column(JSON)
sort_order = Column(Integer, default=0)
category = relationship("FaqCategory", back_populates="questions")
+21
查看文件
@@ -0,0 +1,21 @@
from __future__ import annotations
from sqlalchemy import Column, Integer, String, Text, Boolean, DateTime, JSON
from sqlalchemy.sql import func
from app.database import Base
class GalleryItem(Base):
__tablename__ = "gallery_items"
id = Column(Integer, primary_key=True, autoincrement=True)
title = Column(String(200))
image = Column(String(500), nullable=False)
photographer = Column(String(100))
location = Column(String(200))
description = Column(Text)
tags = Column(JSON) # list of strings
is_visible = Column(Boolean, default=True)
sort_order = Column(Integer, default=0)
created_at = Column(DateTime, server_default=func.now())
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
+34
查看文件
@@ -0,0 +1,34 @@
from __future__ import annotations
from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, JSON
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from app.database import Base
class GuideConfig(Base):
__tablename__ = "guide_config"
id = Column(Integer, primary_key=True, autoincrement=True)
page_intro = Column(Text)
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
sections = relationship("GuideSection", back_populates="config", cascade="all, delete-orphan", order_by="GuideSection.sort_order")
class GuideSection(Base):
"""Each section has unique nested structure, stored as JSON columns."""
__tablename__ = "guide_sections"
id = Column(Integer, primary_key=True, autoincrement=True)
config_id = Column(Integer, ForeignKey("guide_config.id", ondelete="CASCADE"), nullable=False)
section_id = Column(String(50), unique=True, nullable=False)
title = Column(String(200))
subtitle = Column(String(500))
icon = Column(String(10))
content = Column(Text)
# Flexible JSON columns for varied section structures
data = Column(JSON)
sort_order = Column(Integer, default=0)
config = relationship("GuideConfig", back_populates="sections")
+37
查看文件
@@ -0,0 +1,37 @@
from __future__ import annotations
from sqlalchemy import Column, Integer, String, ForeignKey, JSON
from sqlalchemy.orm import relationship
from app.database import Base
class NavHeader(Base):
__tablename__ = "nav_header"
id = Column(Integer, primary_key=True, autoincrement=True)
text = Column(String(100), nullable=False)
to_path = Column(String(255), nullable=False)
children = Column(JSON) # [{text, to, season}, ...] for dropdown menus
sort_order = Column(Integer, default=0)
class NavFooterGroup(Base):
__tablename__ = "nav_footer_groups"
id = Column(Integer, primary_key=True, autoincrement=True)
title = Column(String(100), nullable=False)
sort_order = Column(Integer, default=0)
links = relationship("NavFooterLink", back_populates="group", cascade="all, delete-orphan", order_by="NavFooterLink.sort_order")
class NavFooterLink(Base):
__tablename__ = "nav_footer_links"
id = Column(Integer, primary_key=True, autoincrement=True)
group_id = Column(Integer, ForeignKey("nav_footer_groups.id", ondelete="CASCADE"), nullable=False)
text = Column(String(100), nullable=False)
to_path = Column(String(255), nullable=False)
sort_order = Column(Integer, default=0)
group = relationship("NavFooterGroup", back_populates="links")
+22
查看文件
@@ -0,0 +1,22 @@
from __future__ import annotations
from sqlalchemy import Column, Integer, String, Text, Boolean, DateTime
from sqlalchemy.sql import func
from app.database import Base
class News(Base):
__tablename__ = "news"
id = Column(Integer, primary_key=True, autoincrement=True)
title = Column(String(200), nullable=False)
cover_image = Column(String(500))
summary = Column(Text)
content = Column(Text)
source = Column(String(100))
source_url = Column(String(500))
published_at = Column(String(50))
is_visible = Column(Boolean, default=True)
sort_order = Column(Integer, default=0)
created_at = Column(DateTime, server_default=func.now())
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
+18
查看文件
@@ -0,0 +1,18 @@
from __future__ import annotations
from sqlalchemy import Column, Integer, String, Text, DateTime
from sqlalchemy.sql import func
from app.database import Base
class Partner(Base):
__tablename__ = "partners"
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String(200), nullable=False)
logo = Column(String(500))
website = Column(String(500))
description = Column(Text)
category = Column(String(100))
sort_order = Column(Integer, default=0)
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
+22
查看文件
@@ -0,0 +1,22 @@
from __future__ import annotations
from sqlalchemy import Column, Integer, String, Text, Boolean, DateTime, JSON
from sqlalchemy.sql import func
from app.database import Base
class PricingItem(Base):
__tablename__ = "pricing_items"
id = Column(Integer, primary_key=True, autoincrement=True)
product_name = Column(String(200), nullable=False)
product_slug = Column(String(100))
price_from = Column(String(50))
price_unit = Column(String(50))
price_label = Column(String(100))
description = Column(Text)
features = Column(JSON)
notes = Column(JSON)
is_visible = Column(Boolean, default=True)
sort_order = Column(Integer, default=0)
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
+125
查看文件
@@ -0,0 +1,125 @@
from __future__ import annotations
from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, JSON
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from app.database import Base
class ProductConfig(Base):
__tablename__ = "product_config"
id = Column(Integer, primary_key=True, autoincrement=True)
narrative = Column(Text)
pricing_philosophy = Column(Text)
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
versions = relationship("ProductVersion", back_populates="config", cascade="all, delete-orphan", order_by="ProductVersion.sort_order")
summer_camp = relationship("SummerCamp", back_populates="config", uselist=False, cascade="all, delete-orphan")
selection_guide = relationship("SelectionGuide", back_populates="config", uselist=False, cascade="all, delete-orphan")
class ProductVersion(Base):
__tablename__ = "product_versions"
id = Column(Integer, primary_key=True, autoincrement=True)
config_id = Column(Integer, ForeignKey("product_config.id", ondelete="CASCADE"), nullable=False)
version_id = Column(String(50), unique=True, nullable=False)
name = Column(String(200), nullable=False)
days = Column(Integer, nullable=False)
nights = Column(Integer, nullable=False)
audience = Column(String(255))
description = Column(Text)
tag = Column(String(50))
sort_order = Column(Integer, default=0)
config = relationship("ProductConfig", back_populates="versions")
highlights = relationship("ProductVersionHighlight", back_populates="version", cascade="all, delete-orphan", order_by="ProductVersionHighlight.sort_order")
class ProductVersionHighlight(Base):
__tablename__ = "product_version_highlights"
id = Column(Integer, primary_key=True, autoincrement=True)
version_id = Column(Integer, ForeignKey("product_versions.id", ondelete="CASCADE"), nullable=False)
text = Column(Text, nullable=False)
sort_order = Column(Integer, default=0)
version = relationship("ProductVersion", back_populates="highlights")
class SummerCamp(Base):
__tablename__ = "summer_camp"
id = Column(Integer, primary_key=True, autoincrement=True)
config_id = Column(Integer, ForeignKey("product_config.id", ondelete="CASCADE"), nullable=False)
name = Column(String(200))
positioning = Column(Text)
days = Column(Integer)
nights = Column(Integer)
sessions_json = Column(JSON)
difference_from_v9 = Column(Text)
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
config = relationship("ProductConfig", back_populates="summer_camp")
principles = relationship("SummerCampPrinciple", back_populates="camp", cascade="all, delete-orphan", order_by="SummerCampPrinciple.sort_order")
activities = relationship("SummerCampActivity", back_populates="camp", cascade="all, delete-orphan", order_by="SummerCampActivity.sort_order")
itinerary = relationship("SummerCampItinerary", back_populates="camp", cascade="all, delete-orphan", order_by="SummerCampItinerary.sort_order")
faq = relationship("SummerCampFaq", back_populates="camp", cascade="all, delete-orphan", order_by="SummerCampFaq.sort_order")
class SummerCampPrinciple(Base):
__tablename__ = "summer_camp_principles"
id = Column(Integer, primary_key=True, autoincrement=True)
camp_id = Column(Integer, ForeignKey("summer_camp.id", ondelete="CASCADE"), nullable=False)
text = Column(String(255), nullable=False)
sort_order = Column(Integer, default=0)
camp = relationship("SummerCamp", back_populates="principles")
class SummerCampActivity(Base):
__tablename__ = "summer_camp_activities"
id = Column(Integer, primary_key=True, autoincrement=True)
camp_id = Column(Integer, ForeignKey("summer_camp.id", ondelete="CASCADE"), nullable=False)
text = Column(Text, nullable=False)
sort_order = Column(Integer, default=0)
camp = relationship("SummerCamp", back_populates="activities")
class SummerCampItinerary(Base):
__tablename__ = "summer_camp_itinerary"
id = Column(Integer, primary_key=True, autoincrement=True)
camp_id = Column(Integer, ForeignKey("summer_camp.id", ondelete="CASCADE"), nullable=False)
text = Column(Text, nullable=False)
sort_order = Column(Integer, default=0)
camp = relationship("SummerCamp", back_populates="itinerary")
class SummerCampFaq(Base):
__tablename__ = "summer_camp_faq"
id = Column(Integer, primary_key=True, autoincrement=True)
camp_id = Column(Integer, ForeignKey("summer_camp.id", ondelete="CASCADE"), nullable=False)
question = Column(Text, nullable=False)
answer = Column(Text, nullable=False)
sort_order = Column(Integer, default=0)
camp = relationship("SummerCamp", back_populates="faq")
class SelectionGuide(Base):
__tablename__ = "selection_guide"
id = Column(Integer, primary_key=True, autoincrement=True)
config_id = Column(Integer, ForeignKey("product_config.id", ondelete="CASCADE"), nullable=False)
by_vacation_length = Column(JSON)
by_child_age = Column(JSON)
by_preference = Column(JSON)
config = relationship("ProductConfig", back_populates="selection_guide")
+19
查看文件
@@ -0,0 +1,19 @@
from __future__ import annotations
from sqlalchemy import Column, Integer, String, Text, DateTime
from sqlalchemy.sql import func
from app.database import Base
class Qualification(Base):
__tablename__ = "qualifications"
id = Column(Integer, primary_key=True, autoincrement=True)
title = Column(String(200), nullable=False)
issuer = Column(String(200))
year = Column(String(20))
image = Column(String(500))
description = Column(Text)
category = Column(String(100))
sort_order = Column(Integer, default=0)
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
+32
查看文件
@@ -0,0 +1,32 @@
from __future__ import annotations
from sqlalchemy import Column, Integer, String, Text, Boolean, DateTime, JSON
from sqlalchemy.sql import func
from app.database import Base
class ReviewSummary(Base):
__tablename__ = "review_summary"
id = Column(Integer, primary_key=True, autoincrement=True)
total_count = Column(Integer, default=0)
approval_rate = Column(String(10))
keywords = Column(JSON)
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
class Review(Base):
__tablename__ = "reviews"
id = Column(Integer, primary_key=True, autoincrement=True)
nickname = Column(String(100), nullable=False)
travel_date = Column(String(50))
product_version = Column(String(200))
screenshot = Column(String(500))
content = Column(Text, nullable=False)
scenes = Column(JSON)
concerns = Column(JSON)
sort_order = Column(Integer, default=0)
is_visible = Column(Boolean, default=True)
created_at = Column(DateTime, server_default=func.now())
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
+66
查看文件
@@ -0,0 +1,66 @@
"""
Seasonal products: autumn (游牧的森林) and winter (嗨冰雪).
Both share the same table structure, distinguished by `season` field.
"""
from __future__ import annotations
from sqlalchemy import Column, Integer, String, Text, DateTime, JSON
from sqlalchemy.sql import func
from app.database import Base
class SeasonalProductConfig(Base):
__tablename__ = "seasonal_product_config"
id = Column(Integer, primary_key=True, autoincrement=True)
season = Column(String(20), unique=True, nullable=False) # 'autumn' or 'winter'
narrative = Column(Text)
style = Column(Text)
brand_name = Column(String(100))
version_label = Column(String(100))
season_label = Column(String(100))
selection_guide = Column(JSON) # {byVacationLength: [...]}
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
class SeasonalProductVersion(Base):
__tablename__ = "seasonal_product_versions"
id = Column(Integer, primary_key=True, autoincrement=True)
season = Column(String(20), nullable=False) # 'autumn' or 'winter'
version_id = Column(String(50), unique=True, nullable=False)
name = Column(String(200), nullable=False)
days = Column(Integer, nullable=False)
nights = Column(Integer, nullable=False)
tag = Column(String(50))
line = Column(String(50))
route = Column(String(200))
audience = Column(String(255))
description = Column(Text)
highlights = Column(JSON) # list of strings
itinerary = Column(JSON) # list of strings
sort_order = Column(Integer, default=0)
class SeasonalProductHighlight(Base):
__tablename__ = "seasonal_product_highlights"
id = Column(Integer, primary_key=True, autoincrement=True)
season = Column(String(20), nullable=False)
category = Column(String(20), nullable=False) # 'shared', 'south', 'north'
title = Column(String(200), nullable=False)
description = Column(Text)
sort_order = Column(Integer, default=0)
class SeasonalProductTimeline(Base):
__tablename__ = "seasonal_product_timeline"
id = Column(Integer, primary_key=True, autoincrement=True)
season = Column(String(20), nullable=False)
version = Column(String(50))
date = Column(String(50))
title = Column(String(200))
changes = Column(JSON) # list of {type, text}
reason = Column(Text)
sort_order = Column(Integer, default=0)
+14
查看文件
@@ -0,0 +1,14 @@
from __future__ import annotations
from sqlalchemy import Column, Integer, JSON, DateTime
from sqlalchemy.sql import func
from app.database import Base
class SelectorConfig(Base):
__tablename__ = "selector_config"
id = Column(Integer, primary_key=True, autoincrement=True)
questions = Column(JSON) # list of {id, text, options: [{value, label}]}
rules = Column(JSON) # list of {conditions, recommendation}
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
+19
查看文件
@@ -0,0 +1,19 @@
from __future__ import annotations
from sqlalchemy import Column, Integer, String, Text, DateTime
from sqlalchemy.sql import func
from app.database import Base
class SeoPage(Base):
__tablename__ = "seo_pages"
id = Column(Integer, primary_key=True, autoincrement=True)
page_key = Column(String(50), unique=True, nullable=False)
title = Column(String(500))
description = Column(Text)
h1 = Column(String(500))
keywords = Column(Text)
og_image = Column(String(500))
tldr = Column(Text)
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
+17
查看文件
@@ -0,0 +1,17 @@
from __future__ import annotations
from sqlalchemy import Column, Integer, String, Text
from app.database import Base
class SiteImage(Base):
"""网站图片配置 - 每张图片一条记录,用 group + key 标识"""
__tablename__ = "site_images"
id = Column(Integer, primary_key=True, autoincrement=True)
group_name = Column(String(50), nullable=False) # logo, hero, mascot, team, etc.
image_key = Column(String(50), nullable=False) # main, background, front, etc.
image_path = Column(String(500), nullable=False) # /images/logo.png
label = Column(String(100)) # 显示名称:品牌Logo
size_hint = Column(String(100)) # 建议尺寸:200x200px
description = Column(Text) # 说明
+24
查看文件
@@ -0,0 +1,24 @@
from __future__ import annotations
from sqlalchemy import Column, Integer, String, Text, Boolean, DateTime, JSON
from sqlalchemy.sql import func
from app.database import Base
class Story(Base):
__tablename__ = "stories"
id = Column(Integer, primary_key=True, autoincrement=True)
title = Column(String(200), nullable=False)
cover_image = Column(String(500))
summary = Column(Text)
content = Column(Text)
customer_name = Column(String(100))
avatar = Column(String(500))
travel_date = Column(String(50))
product_name = Column(String(200))
scenes = Column(JSON)
is_visible = Column(Boolean, default=True)
sort_order = Column(Integer, default=0)
created_at = Column(DateTime, server_default=func.now())
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
+18
查看文件
@@ -0,0 +1,18 @@
from __future__ import annotations
from sqlalchemy import Column, Integer, String, Boolean, DateTime, Enum
from sqlalchemy.sql import func
from app.database import Base
class AdminUser(Base):
__tablename__ = "admin_users"
id = Column(Integer, primary_key=True, autoincrement=True)
username = Column(String(50), unique=True, nullable=False)
password_hash = Column(String(255), nullable=False)
display_name = Column(String(100))
role = Column(Enum("admin", "editor"), default="editor")
is_active = Column(Boolean, default=True)
created_at = Column(DateTime, server_default=func.now())
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
+100
查看文件
@@ -0,0 +1,100 @@
from __future__ import annotations
from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, JSON, Enum
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from app.database import Base
class VersionConfig(Base):
__tablename__ = "version_config"
id = Column(Integer, primary_key=True, autoincrement=True)
stats_iterations = Column(Integer)
stats_years = Column(Integer)
stats_guests = Column(String(50))
quote_text = Column(Text)
quote_author = Column(String(200))
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
upgrades = relationship("VersionUpgrade", back_populates="config", cascade="all, delete-orphan", order_by="VersionUpgrade.sort_order")
highlights = relationship("VersionHighlight", back_populates="config", cascade="all, delete-orphan", order_by="VersionHighlight.sort_order")
compare = relationship("VersionCompare", back_populates="config", uselist=False, cascade="all, delete-orphan")
timeline = relationship("VersionTimeline", back_populates="config", cascade="all, delete-orphan", order_by="VersionTimeline.sort_order")
philosophy = relationship("VersionPhilosophy", back_populates="config", cascade="all, delete-orphan", order_by="VersionPhilosophy.sort_order")
class VersionUpgrade(Base):
__tablename__ = "version_upgrades"
id = Column(Integer, primary_key=True, autoincrement=True)
config_id = Column(Integer, ForeignKey("version_config.id", ondelete="CASCADE"), nullable=False)
tag = Column(String(50))
name = Column(String(200))
description = Column(Text)
reason = Column(Text)
sort_order = Column(Integer, default=0)
config = relationship("VersionConfig", back_populates="upgrades")
class VersionHighlight(Base):
__tablename__ = "version_highlights"
id = Column(Integer, primary_key=True, autoincrement=True)
config_id = Column(Integer, ForeignKey("version_config.id", ondelete="CASCADE"), nullable=False)
label = Column(String(50))
text = Column(Text)
sort_order = Column(Integer, default=0)
config = relationship("VersionConfig", back_populates="highlights")
class VersionCompare(Base):
__tablename__ = "version_compare"
id = Column(Integer, primary_key=True, autoincrement=True)
config_id = Column(Integer, ForeignKey("version_config.id", ondelete="CASCADE"), nullable=False)
headers = Column(JSON)
rows = Column(JSON)
config = relationship("VersionConfig", back_populates="compare")
class VersionTimeline(Base):
__tablename__ = "version_timeline"
id = Column(Integer, primary_key=True, autoincrement=True)
config_id = Column(Integer, ForeignKey("version_config.id", ondelete="CASCADE"), nullable=False)
version = Column(String(20), nullable=False)
date = Column(String(20))
title = Column(String(200))
reason = Column(Text)
sort_order = Column(Integer, default=0)
config = relationship("VersionConfig", back_populates="timeline")
changes = relationship("VersionTimelineChange", back_populates="timeline", cascade="all, delete-orphan", order_by="VersionTimelineChange.sort_order")
class VersionTimelineChange(Base):
__tablename__ = "version_timeline_changes"
id = Column(Integer, primary_key=True, autoincrement=True)
timeline_id = Column(Integer, ForeignKey("version_timeline.id", ondelete="CASCADE"), nullable=False)
type = Column(Enum("add", "update", "remove"), nullable=False)
text = Column(Text, nullable=False)
sort_order = Column(Integer, default=0)
timeline = relationship("VersionTimeline", back_populates="changes")
class VersionPhilosophy(Base):
__tablename__ = "version_philosophy"
id = Column(Integer, primary_key=True, autoincrement=True)
config_id = Column(Integer, ForeignKey("version_config.id", ondelete="CASCADE"), nullable=False)
label = Column(String(50))
text = Column(Text)
sort_order = Column(Integer, default=0)
config = relationship("VersionConfig", back_populates="philosophy")
+62
查看文件
@@ -0,0 +1,62 @@
"""Winter camp (小蒙马冬季亲子营) data model."""
from __future__ import annotations
from sqlalchemy import Column, Integer, String, Text, DateTime, JSON
from sqlalchemy.sql import func
from app.database import Base
class WinterCampConfig(Base):
__tablename__ = "winter_camp_config"
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String(200))
positioning = Column(Text)
days = Column(Integer)
nights = Column(Integer)
max_families = Column(Integer)
total_sessions = Column(Integer)
age_range = Column(String(50))
deposit = Column(Integer)
season = Column(String(100))
route = Column(String(200))
why_hulunbuir = Column(Text)
closing_note = Column(Text)
photographer = Column(JSON) # {name, description, specialDay2}
winter_clothing = Column(JSON) # {upper, lower, shoes, accessories}
camp_advantages = Column(JSON) # list of strings
service_config = Column(JSON) # list of strings
camp_essentials = Column(JSON) # list of strings
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
class WinterCampHotel(Base):
__tablename__ = "winter_camp_hotels"
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String(200), nullable=False)
star = Column(String(50))
nights = Column(String(50))
description = Column(Text)
sort_order = Column(Integer, default=0)
class WinterCampItinerary(Base):
__tablename__ = "winter_camp_itinerary"
id = Column(Integer, primary_key=True, autoincrement=True)
day = Column(Integer, nullable=False)
title = Column(String(200))
summary = Column(Text)
highlights = Column(JSON) # list of strings
hotel = Column(String(200))
sort_order = Column(Integer, default=0)
class WinterCampFaq(Base):
__tablename__ = "winter_camp_faq"
id = Column(Integer, primary_key=True, autoincrement=True)
question = Column(Text, nullable=False)
answer = Column(Text, nullable=False)
sort_order = Column(Integer, default=0)