Initial commit: hulai admin API backend

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
这个提交包含在:
刘涛 2026-03-22 22:01:23 +08:00
当前提交 8c0a5f489d
共有 114 个文件被更改,包括 7619 次插入0 次删除

11
.claude/launch.json 普通文件
查看文件

@ -0,0 +1,11 @@
{
"version": "0.0.1",
"configurations": [
{
"name": "hulai-admin-api (FastAPI 后端)",
"runtimeExecutable": "venv/bin/uvicorn",
"runtimeArgs": ["app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"],
"port": 8000
}
]
}

23
.claude/settings.local.json 普通文件
查看文件

@ -0,0 +1,23 @@
{
"permissions": {
"defaultMode": "bypassPermissions",
"allow": [
"Bash(find /Users/liutao/Desktop/hulai-admin-api -type f -name *.py -o -type d)",
"Bash(source venv/bin/activate)",
"Bash(alembic init:*)",
"Bash(alembic revision:*)",
"Bash(alembic upgrade:*)",
"Bash(find /Users/liutao/Desktop/hulai-admin-api/scripts -type f -name *.py)",
"Bash(find /Users/liutao/Desktop/hulai-admin-api -name *.json -type f)",
"Bash(venv/bin/alembic upgrade:*)",
"Bash(venv/bin/alembic current:*)",
"Bash(wc -l /Users/liutao/Desktop/hulai-admin-api/app/routers/*.py)",
"Bash(venv/bin/python -c \":*)",
"Bash(mysql -u root -e \"SHOW DATABASES;\")",
"Bash(mysql -u root hulai_admin -e \"SHOW TABLES;\")",
"Bash(python -c \"import fastapi, uvicorn, sqlalchemy; print\\(''依赖检查通过''\\)\")",
"Bash(python /tmp/test_api.py)",
"Bash(python3:*)"
]
}
}

7
.env.example 普通文件
查看文件

@ -0,0 +1,7 @@
DATABASE_URL=mysql+pymysql://root:password@localhost:3306/hulai_admin?charset=utf8mb4
JWT_SECRET_KEY=change-this-to-a-random-secret-key
JWT_ALGORITHM=HS256
JWT_EXPIRE_MINUTES=480
NUXT_DATA_PATH=/path/to/hulai-website/data
NUXT_PROJECT_PATH=/path/to/hulai-website
UPLOAD_DIR=/path/to/hulai-website/public/images

36
.gitignore vendored 普通文件
查看文件

@ -0,0 +1,36 @@
# Python
venv/
__pycache__/
*.pyc
*.pyo
*.pyd
.Python
*.egg-info/
dist/
build/
.eggs/
# Database
*.db
*.sqlite3
# Uploads & media
uploads/
# Environment
.env
.env.local
.env.*.local
# IDE
.vscode/
.idea/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# Logs
*.log

119
alembic.ini 普通文件
查看文件

@ -0,0 +1,119 @@
# A generic, single database configuration.
[alembic]
# path to migration scripts
# Use forward slashes (/) also on windows to provide an os agnostic path
script_location = migrations
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
# Uncomment the line below if you want the files to be prepended with date and time
# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
# for all available tokens
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
# sys.path path, will be prepended to sys.path if present.
# defaults to the current working directory.
prepend_sys_path = .
# timezone to use when rendering the date within the migration file
# as well as the filename.
# If specified, requires the python>=3.9 or backports.zoneinfo library and tzdata library.
# Any required deps can installed by adding `alembic[tz]` to the pip requirements
# string value is passed to ZoneInfo()
# leave blank for localtime
# timezone =
# max length of characters to apply to the "slug" field
# truncate_slug_length = 40
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# set to 'true' to allow .pyc and .pyo files without
# a source .py file to be detected as revisions in the
# versions/ directory
# sourceless = false
# version location specification; This defaults
# to migrations/versions. When using multiple version
# directories, initial revisions must be specified with --version-path.
# The path separator used here should be the separator specified by "version_path_separator" below.
# version_locations = %(here)s/bar:%(here)s/bat:migrations/versions
# version path separator; As mentioned above, this is the character used to split
# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
# Valid values for version_path_separator are:
#
# version_path_separator = :
# version_path_separator = ;
# version_path_separator = space
# version_path_separator = newline
#
# Use os.pathsep. Default configuration used for new projects.
version_path_separator = os
# set to 'true' to search source files recursively
# in each "version_locations" directory
# new in Alembic version 1.10
# recursive_version_locations = false
# the output encoding used when revision files
# are written from script.py.mako
# output_encoding = utf-8
sqlalchemy.url = driver://user:pass@localhost/dbname
[post_write_hooks]
# post_write_hooks defines scripts or Python functions that are run
# on newly generated revision scripts. See the documentation for further
# detail and examples
# format using "black" - use the console_scripts runner, against the "black" entrypoint
# hooks = black
# black.type = console_scripts
# black.entrypoint = black
# black.options = -l 79 REVISION_SCRIPT_FILENAME
# lint with attempts to fix using "ruff" - use the exec runner, execute a binary
# hooks = ruff
# ruff.type = exec
# ruff.executable = %(here)s/.venv/bin/ruff
# ruff.options = --fix REVISION_SCRIPT_FILENAME
# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARNING
handlers = console
qualname =
[logger_sqlalchemy]
level = WARNING
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S

0
app/__init__.py 普通文件
查看文件

51
app/auth.py 普通文件
查看文件

@ -0,0 +1,51 @@
from __future__ import annotations
from datetime import datetime, timedelta, timezone
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from jose import JWTError, jwt
from passlib.context import CryptContext
from sqlalchemy.orm import Session
from app.config import settings
from app.database import get_db
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/login")
def hash_password(password: str) -> str:
return pwd_context.hash(password)
def verify_password(plain: str, hashed: str) -> bool:
return pwd_context.verify(plain, hashed)
def create_access_token(data: dict, expires_delta: timedelta | None = None) -> str:
to_encode = data.copy()
expire = datetime.now(timezone.utc) + (expires_delta or timedelta(minutes=settings.JWT_EXPIRE_MINUTES))
to_encode.update({"exp": expire})
return jwt.encode(to_encode, settings.JWT_SECRET_KEY, algorithm=settings.JWT_ALGORITHM)
def get_current_user(token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)):
from app.models.user import AdminUser
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="无效的认证凭据",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, settings.JWT_SECRET_KEY, algorithms=[settings.JWT_ALGORITHM])
username: str = payload.get("sub")
if username is None:
raise credentials_exception
except JWTError:
raise credentials_exception
user = db.query(AdminUser).filter(AdminUser.username == username, AdminUser.is_active == True).first()
if user is None:
raise credentials_exception
return user

18
app/config.py 普通文件
查看文件

@ -0,0 +1,18 @@
from __future__ import annotations
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
DATABASE_URL: str
JWT_SECRET_KEY: str
JWT_ALGORITHM: str = "HS256"
JWT_EXPIRE_MINUTES: int = 480
NUXT_DATA_PATH: str
NUXT_PROJECT_PATH: str
UPLOAD_DIR: str
class Config:
env_file = ".env"
settings = Settings()

20
app/database.py 普通文件
查看文件

@ -0,0 +1,20 @@
from __future__ import annotations
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, DeclarativeBase
from app.config import settings
engine = create_engine(settings.DATABASE_URL, echo=False, pool_pre_ping=True)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
class Base(DeclarativeBase):
pass
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()

62
app/main.py 普通文件
查看文件

@ -0,0 +1,62 @@
from __future__ import annotations
import os
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app.routers import auth, brand, reviews, faq, contact, seo, navigation, products, about, versions, guides, upload, export, site_images
from app.routers.seasonal_products import autumn_router, winter_router
from app.routers import winter_camp, destinations, blog, stories, news, pricing, qualifications, partners, calendar
from app.routers import destinations_detail, gallery, courses, selector, customize
app = FastAPI(title="呼籁旅行管理后台 API", version="1.0.0", redirect_slashes=False)
# CORS - 根据环境区分
_cors_origins = ["https://admin.1814.love"]
if os.getenv("ENV", "development") == "development":
_cors_origins += ["http://localhost:5173", "http://localhost:3001"]
app.add_middleware(
CORSMiddleware,
allow_origins=_cors_origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Register routers
app.include_router(auth.router)
app.include_router(brand.router)
app.include_router(products.router)
app.include_router(faq.router)
app.include_router(reviews.router)
app.include_router(about.router)
app.include_router(contact.router)
app.include_router(seo.router)
app.include_router(navigation.router)
app.include_router(versions.router)
app.include_router(guides.router)
app.include_router(upload.router)
app.include_router(site_images.router)
app.include_router(export.router)
app.include_router(autumn_router)
app.include_router(winter_router)
app.include_router(winter_camp.router)
app.include_router(destinations.router)
app.include_router(blog.router)
app.include_router(stories.router)
app.include_router(news.router)
app.include_router(pricing.router)
app.include_router(qualifications.router)
app.include_router(partners.router)
app.include_router(calendar.router)
app.include_router(destinations_detail.router)
app.include_router(gallery.router)
app.include_router(courses.router)
app.include_router(selector.router)
app.include_router(customize.router)
@app.get("/api/health")
def health_check():
return {"status": "ok", "service": "hulai-admin-api"}

28
app/models/__init__.py 普通文件
查看文件

@ -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
app/models/about.py 普通文件
查看文件

@ -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
app/models/blog.py 普通文件
查看文件

@ -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
app/models/brand.py 普通文件
查看文件

@ -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
app/models/calendar.py 普通文件
查看文件

@ -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
app/models/contact.py 普通文件
查看文件

@ -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
app/models/courses.py 普通文件
查看文件

@ -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())

查看文件

@ -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())

查看文件

@ -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
app/models/destination.py 普通文件
查看文件

@ -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)

查看文件

@ -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
app/models/export_log.py 普通文件
查看文件

@ -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
app/models/faq.py 普通文件
查看文件

@ -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
app/models/gallery.py 普通文件
查看文件

@ -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
app/models/guide.py 普通文件
查看文件

@ -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
app/models/navigation.py 普通文件
查看文件

@ -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
app/models/news.py 普通文件
查看文件

@ -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
app/models/partner.py 普通文件
查看文件

@ -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
app/models/pricing.py 普通文件
查看文件

@ -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
app/models/product.py 普通文件
查看文件

@ -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
app/models/qualification.py 普通文件
查看文件

@ -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
app/models/review.py 普通文件
查看文件

@ -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())

查看文件

@ -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
app/models/selector.py 普通文件
查看文件

@ -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
app/models/seo.py 普通文件
查看文件

@ -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
app/models/site_image.py 普通文件
查看文件

@ -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
app/models/story.py 普通文件
查看文件

@ -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
app/models/user.py 普通文件
查看文件

@ -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
app/models/version.py 普通文件
查看文件

@ -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
app/models/winter_camp.py 普通文件
查看文件

@ -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)

0
app/routers/__init__.py 普通文件
查看文件

361
app/routers/about.py 普通文件
查看文件

@ -0,0 +1,361 @@
from __future__ import annotations
from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from app.database import get_db
from app.auth import get_current_user
from app.models.about import (
AboutStory, AboutSubsidiary, AboutCertification, AboutTrademark,
AboutCopyright, AboutGuarantee, AboutXiaohongshu, AboutTeam,
AboutCulture, AboutCultureValue,
AboutFounderDetail, AboutFounder, AboutMilestone,
AboutServicePrinciple, AboutDifferentiation,
AboutServiceJourney, AboutServiceMoment, AboutStats,
)
from app.schemas.about import (
StorySchema, SubsidiarySchema, CertificationSchema, TrademarkSchema,
CopyrightSchema, GuaranteeSchema, XiaohongshuSchema, TeamSchema,
CultureSchema, FounderDetailSchema, FounderSchema, MilestoneSchema,
ServicePrincipleSchema, DifferentiationSchema,
ServiceJourneySchema, ServiceMomentSchema, AboutStatsSchema,
)
router = APIRouter(prefix="/api/about", tags=["关于我们"], dependencies=[Depends(get_current_user)])
# --- Story ---
@router.get("/story", response_model=StorySchema)
def get_story(db: Session = Depends(get_db)):
story = db.query(AboutStory).first()
if not story:
return StorySchema()
return StorySchema(
title=story.title, content=story.content, founding_moment=story.founding_moment,
totem_description=story.totem_description, totem_tagline=story.totem_tagline,
)
@router.put("/story")
def update_story(req: StorySchema, db: Session = Depends(get_db)):
story = db.query(AboutStory).first()
if not story:
story = AboutStory()
db.add(story)
story.title = req.title
story.content = req.content
story.founding_moment = req.founding_moment
story.totem_description = req.totem_description
story.totem_tagline = req.totem_tagline
db.commit()
return {"message": "更新成功"}
# --- Subsidiaries ---
@router.get("/subsidiaries", response_model=list[SubsidiarySchema])
def get_subsidiaries(db: Session = Depends(get_db)):
items = db.query(AboutSubsidiary).order_by(AboutSubsidiary.sort_order).all()
return [SubsidiarySchema(name=s.name, role=s.role, established=s.established) for s in items]
@router.put("/subsidiaries")
def update_subsidiaries(req: list[SubsidiarySchema], db: Session = Depends(get_db)):
db.query(AboutSubsidiary).delete()
for i, s in enumerate(req):
db.add(AboutSubsidiary(name=s.name, role=s.role, established=s.established, sort_order=i))
db.commit()
return {"message": "更新成功"}
# --- Certifications ---
@router.get("/certifications", response_model=list[CertificationSchema])
def get_certifications(db: Session = Depends(get_db)):
items = db.query(AboutCertification).order_by(AboutCertification.sort_order).all()
return [CertificationSchema(title=c.title, detail=c.detail) for c in items]
@router.put("/certifications")
def update_certifications(req: list[CertificationSchema], db: Session = Depends(get_db)):
db.query(AboutCertification).delete()
for i, c in enumerate(req):
db.add(AboutCertification(title=c.title, detail=c.detail, sort_order=i))
db.commit()
return {"message": "更新成功"}
# --- Trademark ---
@router.get("/trademark", response_model=TrademarkSchema)
def get_trademark(db: Session = Depends(get_db)):
tm = db.query(AboutTrademark).first()
if not tm:
return TrademarkSchema()
return TrademarkSchema(name=tm.name, scope=tm.scope, holder=tm.holder, description=tm.description)
@router.put("/trademark")
def update_trademark(req: TrademarkSchema, db: Session = Depends(get_db)):
tm = db.query(AboutTrademark).first()
if not tm:
tm = AboutTrademark()
db.add(tm)
tm.name = req.name
tm.scope = req.scope
tm.holder = req.holder
tm.description = req.description
db.commit()
return {"message": "更新成功"}
# --- Copyrights ---
@router.get("/copyrights", response_model=list[CopyrightSchema])
def get_copyrights(db: Session = Depends(get_db)):
items = db.query(AboutCopyright).order_by(AboutCopyright.sort_order).all()
return [CopyrightSchema(name=c.name, reg_no=c.reg_no, category=c.category, holder=c.holder, date=c.date, description=c.description) for c in items]
@router.put("/copyrights")
def update_copyrights(req: list[CopyrightSchema], db: Session = Depends(get_db)):
db.query(AboutCopyright).delete()
for i, c in enumerate(req):
db.add(AboutCopyright(name=c.name, reg_no=c.reg_no, category=c.category, holder=c.holder, date=c.date, description=c.description, sort_order=i))
db.commit()
return {"message": "更新成功"}
# --- Guarantees ---
@router.get("/guarantees", response_model=list[GuaranteeSchema])
def get_guarantees(db: Session = Depends(get_db)):
items = db.query(AboutGuarantee).order_by(AboutGuarantee.sort_order).all()
return [GuaranteeSchema(title=g.title, detail=g.detail) for g in items]
@router.put("/guarantees")
def update_guarantees(req: list[GuaranteeSchema], db: Session = Depends(get_db)):
db.query(AboutGuarantee).delete()
for i, g in enumerate(req):
db.add(AboutGuarantee(title=g.title, detail=g.detail, sort_order=i))
db.commit()
return {"message": "更新成功"}
# --- Xiaohongshu ---
@router.get("/xiaohongshu", response_model=XiaohongshuSchema)
def get_xiaohongshu(db: Session = Depends(get_db)):
xhs = db.query(AboutXiaohongshu).first()
if not xhs:
return XiaohongshuSchema()
return XiaohongshuSchema(
account=xhs.account, verified=xhs.verified, verified_type=xhs.verified_type,
followers=xhs.followers, likes=xhs.likes, awards=xhs.awards or [],
tagline=xhs.tagline, tags=xhs.tags or [], description=xhs.description,
)
@router.put("/xiaohongshu")
def update_xiaohongshu(req: XiaohongshuSchema, db: Session = Depends(get_db)):
xhs = db.query(AboutXiaohongshu).first()
if not xhs:
xhs = AboutXiaohongshu()
db.add(xhs)
for key, val in req.model_dump().items():
setattr(xhs, key, val)
db.commit()
return {"message": "更新成功"}
# --- Team ---
@router.get("/team", response_model=TeamSchema)
def get_team(db: Session = Depends(get_db)):
team = db.query(AboutTeam).first()
if not team:
return TeamSchema()
return TeamSchema(summary=team.summary)
@router.put("/team")
def update_team(req: TeamSchema, db: Session = Depends(get_db)):
team = db.query(AboutTeam).first()
if not team:
team = AboutTeam()
db.add(team)
team.summary = req.summary
db.commit()
return {"message": "更新成功"}
# --- Culture ---
@router.get("/culture", response_model=CultureSchema)
def get_culture(db: Session = Depends(get_db)):
culture = db.query(AboutCulture).first()
if not culture:
return CultureSchema()
return CultureSchema(
core=culture.core,
transparency=culture.transparency,
trust=culture.trust,
grief_award_name=culture.grief_award_name,
grief_award_description=culture.grief_award_description,
values=[{"name": v.name, "expression": v.expression} for v in culture.values],
)
@router.put("/culture")
def update_culture(req: CultureSchema, db: Session = Depends(get_db)):
culture = db.query(AboutCulture).first()
if not culture:
culture = AboutCulture()
db.add(culture)
db.flush()
culture.core = req.core
culture.transparency = req.transparency
culture.trust = req.trust
culture.grief_award_name = req.grief_award_name
culture.grief_award_description = req.grief_award_description
db.query(AboutCultureValue).filter_by(culture_id=culture.id).delete()
for i, v in enumerate(req.values):
db.add(AboutCultureValue(culture_id=culture.id, name=v.name, expression=v.expression, sort_order=i))
db.commit()
return {"message": "更新成功"}
# --- Founder Detail (单个创始人详细) ---
@router.get("/founder-detail", response_model=FounderDetailSchema)
def get_founder_detail(db: Session = Depends(get_db)):
fd = db.query(AboutFounderDetail).first()
if not fd:
return FounderDetailSchema()
return FounderDetailSchema(
name=fd.name, title=fd.title, brand_founded=fd.brand_founded,
years_in_hulunbuir=fd.years_in_hulunbuir, background=fd.background,
expertise=fd.expertise or [], media_presence=fd.media_presence or [],
)
@router.put("/founder-detail")
def update_founder_detail(req: FounderDetailSchema, db: Session = Depends(get_db)):
fd = db.query(AboutFounderDetail).first()
if not fd:
fd = AboutFounderDetail()
db.add(fd)
fd.name = req.name
fd.title = req.title
fd.brand_founded = req.brand_founded
fd.years_in_hulunbuir = req.years_in_hulunbuir
fd.background = req.background
fd.expertise = req.expertise
fd.media_presence = req.media_presence
db.commit()
return {"message": "更新成功"}
# --- Founders (三位创始人简介列表) ---
@router.get("/founders", response_model=list[FounderSchema])
def get_founders(db: Session = Depends(get_db)):
items = db.query(AboutFounder).order_by(AboutFounder.sort_order).all()
return [FounderSchema(name=f.name, title=f.title, story=f.story) for f in items]
@router.put("/founders")
def update_founders(req: list[FounderSchema], db: Session = Depends(get_db)):
db.query(AboutFounder).delete()
for i, f in enumerate(req):
db.add(AboutFounder(name=f.name, title=f.title, story=f.story, sort_order=i))
db.commit()
return {"message": "更新成功"}
# --- Milestones ---
@router.get("/milestones", response_model=list[MilestoneSchema])
def get_milestones(db: Session = Depends(get_db)):
items = db.query(AboutMilestone).order_by(AboutMilestone.sort_order).all()
return [MilestoneSchema(year=m.year, event=m.event) for m in items]
@router.put("/milestones")
def update_milestones(req: list[MilestoneSchema], db: Session = Depends(get_db)):
db.query(AboutMilestone).delete()
for i, m in enumerate(req):
db.add(AboutMilestone(year=m.year, event=m.event, sort_order=i))
db.commit()
return {"message": "更新成功"}
# --- Service Principles ---
@router.get("/service-principles", response_model=list[ServicePrincipleSchema])
def get_service_principles(db: Session = Depends(get_db)):
items = db.query(AboutServicePrinciple).order_by(AboutServicePrinciple.sort_order).all()
return [ServicePrincipleSchema(text=p.text) for p in items]
@router.put("/service-principles")
def update_service_principles(req: list[ServicePrincipleSchema], db: Session = Depends(get_db)):
db.query(AboutServicePrinciple).delete()
for i, p in enumerate(req):
db.add(AboutServicePrinciple(text=p.text, sort_order=i))
db.commit()
return {"message": "更新成功"}
# --- Differentiation ---
@router.get("/differentiation", response_model=list[DifferentiationSchema])
def get_differentiation(db: Session = Depends(get_db)):
items = db.query(AboutDifferentiation).order_by(AboutDifferentiation.sort_order).all()
return [DifferentiationSchema(name=d.name, detail=d.detail) for d in items]
@router.put("/differentiation")
def update_differentiation(req: list[DifferentiationSchema], db: Session = Depends(get_db)):
db.query(AboutDifferentiation).delete()
for i, d in enumerate(req):
db.add(AboutDifferentiation(name=d.name, detail=d.detail, sort_order=i))
db.commit()
return {"message": "更新成功"}
# --- Service Journey ---
@router.get("/service-journey", response_model=ServiceJourneySchema)
def get_service_journey(db: Session = Depends(get_db)):
sj = db.query(AboutServiceJourney).first()
if not sj:
return ServiceJourneySchema()
return ServiceJourneySchema(
title=sj.title,
subtitle=sj.subtitle,
moments=[ServiceMomentSchema(step=m.step, name=m.name, detail=m.detail) for m in sj.moments],
)
@router.put("/service-journey")
def update_service_journey(req: ServiceJourneySchema, db: Session = Depends(get_db)):
sj = db.query(AboutServiceJourney).first()
if not sj:
sj = AboutServiceJourney()
db.add(sj)
db.flush()
sj.title = req.title
sj.subtitle = req.subtitle
db.query(AboutServiceMoment).filter_by(journey_id=sj.id).delete()
for i, m in enumerate(req.moments):
db.add(AboutServiceMoment(journey_id=sj.id, step=m.step, name=m.name, detail=m.detail, sort_order=i))
db.commit()
return {"message": "更新成功"}
# --- Stats ---
@router.get("/stats", response_model=AboutStatsSchema)
def get_stats(db: Session = Depends(get_db)):
s = db.query(AboutStats).first()
if not s:
return AboutStatsSchema()
return AboutStatsSchema(data=s.data or {})
@router.put("/stats")
def update_stats(req: AboutStatsSchema, db: Session = Depends(get_db)):
s = db.query(AboutStats).first()
if not s:
s = AboutStats()
db.add(s)
s.data = req.data
db.commit()
return {"message": "更新成功"}

104
app/routers/auth.py 普通文件
查看文件

@ -0,0 +1,104 @@
from __future__ import annotations
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from app.database import get_db
from app.auth import verify_password, create_access_token, hash_password, get_current_user
from app.models.user import AdminUser
from app.schemas.user import LoginRequest, LoginResponse, UserInfo, ChangePasswordRequest, CreateUserRequest, UpdateUserRequest, ResetPasswordRequest, UserListResponse
router = APIRouter(prefix="/api/auth", tags=["认证"])
@router.post("/login", response_model=LoginResponse)
def login(req: LoginRequest, db: Session = Depends(get_db)):
user = db.query(AdminUser).filter(AdminUser.username == req.username).first()
if not user or not verify_password(req.password, user.password_hash):
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="用户名或密码错误")
if not user.is_active:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="账号已被禁用")
token = create_access_token({"sub": user.username, "role": user.role})
return LoginResponse(access_token=token)
@router.get("/me", response_model=UserInfo)
def get_me(current_user: AdminUser = Depends(get_current_user)):
return current_user
@router.put("/password")
def change_password(req: ChangePasswordRequest, current_user: AdminUser = Depends(get_current_user), db: Session = Depends(get_db)):
if not verify_password(req.old_password, current_user.password_hash):
raise HTTPException(status_code=400, detail="原密码错误")
current_user.password_hash = hash_password(req.new_password)
db.commit()
return {"message": "密码修改成功"}
# ===== 用户管理(仅 admin 角色可用)=====
def require_admin(current_user: AdminUser = Depends(get_current_user)):
if current_user.role != "admin":
raise HTTPException(status_code=403, detail="仅管理员可操作")
return current_user
@router.get("/users", response_model=list[UserListResponse])
def list_users(db: Session = Depends(get_db), _: AdminUser = Depends(require_admin)):
users = db.query(AdminUser).order_by(AdminUser.id).all()
return [UserListResponse.model_validate(u) for u in users]
@router.post("/users", response_model=UserListResponse, status_code=201)
def create_user(req: CreateUserRequest, db: Session = Depends(get_db), _: AdminUser = Depends(require_admin)):
if db.query(AdminUser).filter(AdminUser.username == req.username).first():
raise HTTPException(status_code=400, detail="用户名已存在")
user = AdminUser(
username=req.username,
password_hash=hash_password(req.password),
display_name=req.display_name,
role=req.role,
is_active=True,
)
db.add(user)
db.commit()
db.refresh(user)
return UserListResponse.model_validate(user)
@router.put("/users/{user_id}", response_model=UserListResponse)
def update_user(user_id: int, req: UpdateUserRequest, db: Session = Depends(get_db), _: AdminUser = Depends(require_admin)):
user = db.query(AdminUser).get(user_id)
if not user:
raise HTTPException(status_code=404, detail="用户不存在")
if req.display_name is not None:
user.display_name = req.display_name
if req.role is not None:
user.role = req.role
if req.is_active is not None:
user.is_active = req.is_active
db.commit()
db.refresh(user)
return UserListResponse.model_validate(user)
@router.put("/users/{user_id}/reset-password")
def reset_password(user_id: int, req: ResetPasswordRequest, db: Session = Depends(get_db), _: AdminUser = Depends(require_admin)):
user = db.query(AdminUser).get(user_id)
if not user:
raise HTTPException(status_code=404, detail="用户不存在")
user.password_hash = hash_password(req.new_password)
db.commit()
return {"message": f"已重置 {user.username} 的密码"}
@router.delete("/users/{user_id}")
def delete_user(user_id: int, db: Session = Depends(get_db), current_user: AdminUser = Depends(require_admin)):
user = db.query(AdminUser).get(user_id)
if not user:
raise HTTPException(status_code=404, detail="用户不存在")
if user.id == current_user.id:
raise HTTPException(status_code=400, detail="不能删除自己")
db.delete(user)
db.commit()
return {"message": "删除成功"}

74
app/routers/blog.py 普通文件
查看文件

@ -0,0 +1,74 @@
from __future__ import annotations
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session
from sqlalchemy import func as sqlfunc
from app.database import get_db
from app.auth import get_current_user
from app.models.blog import Blog
from app.schemas.blog import BlogSchema, BlogResponse, BlogListResponse
router = APIRouter(prefix="/api/blog", tags=["博客文章"], dependencies=[Depends(get_current_user)])
@router.get("/", response_model=BlogListResponse)
def list_blogs(
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=100),
keyword: str = Query("", description="搜索关键词"),
db: Session = Depends(get_db),
):
query = db.query(Blog)
if keyword:
query = query.filter(Blog.title.contains(keyword) | Blog.summary.contains(keyword))
total = query.count()
items = query.order_by(Blog.sort_order, Blog.id.desc()).offset((page - 1) * page_size).limit(page_size).all()
return BlogListResponse(
items=[BlogResponse.model_validate(b) for b in items],
total=total,
)
@router.post("/", response_model=BlogResponse, status_code=201)
def create_blog(req: BlogSchema, db: Session = Depends(get_db)):
if db.query(Blog).filter(Blog.slug == req.slug).first():
raise HTTPException(status_code=400, detail="该 slug 已存在")
max_order = db.query(sqlfunc.max(Blog.sort_order)).scalar() or 0
blog = Blog(**req.model_dump(), sort_order=max_order + 1)
db.add(blog)
db.commit()
db.refresh(blog)
return BlogResponse.model_validate(blog)
@router.put("/{blog_id}", response_model=BlogResponse)
def update_blog(blog_id: int, req: BlogSchema, db: Session = Depends(get_db)):
blog = db.query(Blog).get(blog_id)
if not blog:
raise HTTPException(status_code=404, detail="文章不存在")
existing = db.query(Blog).filter(Blog.slug == req.slug, Blog.id != blog_id).first()
if existing:
raise HTTPException(status_code=400, detail="该 slug 已被其他文章使用")
for key, val in req.model_dump().items():
setattr(blog, key, val)
db.commit()
db.refresh(blog)
return BlogResponse.model_validate(blog)
@router.delete("/{blog_id}")
def delete_blog(blog_id: int, db: Session = Depends(get_db)):
blog = db.query(Blog).get(blog_id)
if not blog:
raise HTTPException(status_code=404, detail="文章不存在")
db.delete(blog)
db.commit()
return {"message": "删除成功"}
@router.put("/reorder/batch")
def reorder_blogs(ids: list[int], db: Session = Depends(get_db)):
for i, bid in enumerate(ids):
db.query(Blog).filter(Blog.id == bid).update({"sort_order": i})
db.commit()
return {"message": "排序更新成功"}

59
app/routers/brand.py 普通文件
查看文件

@ -0,0 +1,59 @@
from __future__ import annotations
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from app.database import get_db
from app.auth import get_current_user
from app.models.brand import Brand, BrandTrustStat, BrandDifferentiator
from app.schemas.brand import BrandFullSchema, BrandUpdateRequest
router = APIRouter(prefix="/api/brand", tags=["品牌"], dependencies=[Depends(get_current_user)])
@router.get("/", response_model=BrandFullSchema)
def get_brand(db: Session = Depends(get_db)):
brand = db.query(Brand).first()
if not brand:
raise HTTPException(status_code=404, detail="品牌信息未初始化")
return BrandFullSchema(
id=brand.id,
name=brand.name,
full_name=brand.full_name,
domain=brand.domain,
url=brand.url,
slogan_emotional=brand.slogan_emotional,
slogan_functional=brand.slogan_functional,
icp_entity=brand.icp_entity,
icp=brand.icp,
icp_url=brand.icp_url,
e_contract=brand.e_contract,
trust_stats=[{"value": s.value, "unit": s.unit, "label": s.label} for s in brand.trust_stats],
differentiators=[{"title": d.title, "description": d.description} for d in brand.differentiators],
cta_buttons=brand.cta_buttons,
conversion_path=brand.conversion_path,
)
@router.put("/")
def update_brand(req: BrandUpdateRequest, db: Session = Depends(get_db)):
brand = db.query(Brand).first()
if not brand:
brand = Brand()
db.add(brand)
for key, val in req.brand.model_dump().items():
setattr(brand, key, val)
db.flush()
# Replace trust stats
db.query(BrandTrustStat).filter(BrandTrustStat.brand_id == brand.id).delete()
for i, stat in enumerate(req.trust_stats):
db.add(BrandTrustStat(brand_id=brand.id, value=stat.value, unit=stat.unit, label=stat.label, sort_order=i))
# Replace differentiators
db.query(BrandDifferentiator).filter(BrandDifferentiator.brand_id == brand.id).delete()
for i, diff in enumerate(req.differentiators):
db.add(BrandDifferentiator(brand_id=brand.id, title=diff.title, description=diff.description, sort_order=i))
db.commit()
return {"message": "品牌信息更新成功"}

65
app/routers/calendar.py 普通文件
查看文件

@ -0,0 +1,65 @@
from __future__ import annotations
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session
from app.database import get_db
from app.auth import get_current_user
from app.models.calendar import CalendarMonth
from app.schemas.calendar import CalendarMonthSchema, CalendarMonthResponse
router = APIRouter(prefix="/api/calendar", tags=["行程日历"], dependencies=[Depends(get_current_user)])
MONTH_NAMES = ["一月", "二月", "三月", "四月", "五月", "六月", "七月", "八月", "九月", "十月", "十一月", "十二月"]
@router.get("/", response_model=list[CalendarMonthResponse])
def list_calendar(
year: int = Query(..., description="年份,如 2025"),
db: Session = Depends(get_db),
):
items = db.query(CalendarMonth).filter(CalendarMonth.year == year).order_by(CalendarMonth.month).all()
# 如果该年份还没有数据,自动初始化12个月份
if not items:
for m in range(1, 13):
month_record = CalendarMonth(
year=year,
month=m,
title=MONTH_NAMES[m - 1],
highlights=[],
events=[],
is_available=False,
)
db.add(month_record)
db.commit()
items = db.query(CalendarMonth).filter(CalendarMonth.year == year).order_by(CalendarMonth.month).all()
return [CalendarMonthResponse.model_validate(item) for item in items]
@router.put("/{year}/{month}", response_model=CalendarMonthResponse)
def update_calendar_month(year: int, month: int, req: CalendarMonthSchema, db: Session = Depends(get_db)):
item = db.query(CalendarMonth).filter(CalendarMonth.year == year, CalendarMonth.month == month).first()
if not item:
raise HTTPException(status_code=404, detail="日历月份不存在")
for key, val in req.model_dump().items():
setattr(item, key, val)
db.commit()
db.refresh(item)
return CalendarMonthResponse.model_validate(item)
@router.post("/init")
def init_calendar_year(year: int = Query(..., description="要初始化的年份"), db: Session = Depends(get_db)):
existing = db.query(CalendarMonth).filter(CalendarMonth.year == year).count()
if existing > 0:
return {"message": f"{year}年日历已存在,共{existing}条记录"}
for m in range(1, 13):
db.add(CalendarMonth(
year=year,
month=m,
title=MONTH_NAMES[m - 1],
highlights=[],
events=[],
is_available=False,
))
db.commit()
return {"message": f"{year}年12个月份初始化成功"}

68
app/routers/contact.py 普通文件
查看文件

@ -0,0 +1,68 @@
from __future__ import annotations
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from sqlalchemy import func as sqlfunc
from app.database import get_db
from app.auth import get_current_user
from app.models.contact import ContactChannel, ContactConfig
from app.schemas.contact import ContactChannelSchema, ContactChannelResponse, ContactConfigSchema
router = APIRouter(prefix="/api/contact", tags=["联系方式"], dependencies=[Depends(get_current_user)])
@router.get("/channels", response_model=list[ContactChannelResponse])
def list_channels(db: Session = Depends(get_db)):
channels = db.query(ContactChannel).order_by(ContactChannel.sort_order).all()
return [ContactChannelResponse.model_validate(c) for c in channels]
@router.post("/channels", response_model=ContactChannelResponse, status_code=201)
def create_channel(req: ContactChannelSchema, db: Session = Depends(get_db)):
max_order = db.query(sqlfunc.max(ContactChannel.sort_order)).scalar() or 0
ch = ContactChannel(**req.model_dump(), sort_order=max_order + 1)
db.add(ch)
db.commit()
db.refresh(ch)
return ContactChannelResponse.model_validate(ch)
@router.put("/channels/{ch_id}", response_model=ContactChannelResponse)
def update_channel(ch_id: int, req: ContactChannelSchema, db: Session = Depends(get_db)):
ch = db.query(ContactChannel).get(ch_id)
if not ch:
raise HTTPException(status_code=404, detail="联系方式不存在")
for key, val in req.model_dump().items():
setattr(ch, key, val)
db.commit()
db.refresh(ch)
return ContactChannelResponse.model_validate(ch)
@router.delete("/channels/{ch_id}")
def delete_channel(ch_id: int, db: Session = Depends(get_db)):
ch = db.query(ContactChannel).get(ch_id)
if not ch:
raise HTTPException(status_code=404, detail="联系方式不存在")
db.delete(ch)
db.commit()
return {"message": "删除成功"}
@router.get("/config", response_model=ContactConfigSchema)
def get_config(db: Session = Depends(get_db)):
config = db.query(ContactConfig).first()
if not config:
return ContactConfigSchema()
return ContactConfigSchema(security_notice=config.security_notice)
@router.put("/config")
def update_config(req: ContactConfigSchema, db: Session = Depends(get_db)):
config = db.query(ContactConfig).first()
if not config:
config = ContactConfig()
db.add(config)
config.security_notice = req.security_notice
db.commit()
return {"message": "更新成功"}

39
app/routers/courses.py 普通文件
查看文件

@ -0,0 +1,39 @@
from __future__ import annotations
from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from app.database import get_db
from app.auth import get_current_user
from app.models.courses import CoursesConfig
from app.schemas.courses import CoursesConfigSchema, CoursesConfigResponse
router = APIRouter(
prefix="/api/courses",
tags=["研学课程"],
dependencies=[Depends(get_current_user)],
)
def _get_or_create(db: Session) -> CoursesConfig:
config = db.query(CoursesConfig).first()
if not config:
config = CoursesConfig(modules=[], age_groups=[], faqs=[])
db.add(config)
db.commit()
return config
@router.get("/", response_model=CoursesConfigResponse)
def get_config(db: Session = Depends(get_db)):
return CoursesConfigResponse.model_validate(_get_or_create(db))
@router.put("/", response_model=CoursesConfigResponse)
def update_config(req: CoursesConfigSchema, db: Session = Depends(get_db)):
config = _get_or_create(db)
config.modules = req.modules
config.age_groups = req.age_groups
config.faqs = req.faqs
db.commit()
db.refresh(config)
return CoursesConfigResponse.model_validate(config)

104
app/routers/customize.py 普通文件
查看文件

@ -0,0 +1,104 @@
from __future__ import annotations
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session
from app.database import get_db
from app.auth import get_current_user
from app.models.customize_submission import CustomizeSubmission
from app.models.customize_config import CustomizeConfig
from app.schemas.customize_submission import (
CustomizeSubmissionResponse,
CustomizeSubmissionListResponse,
CustomizeStatusUpdate,
)
from app.schemas.customize_config import CustomizeConfigSchema
router = APIRouter(
prefix="/api/customize",
tags=["定制表单"],
dependencies=[Depends(get_current_user)],
)
@router.get("/submissions", response_model=CustomizeSubmissionListResponse)
def list_submissions(
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=100),
keyword: str = Query("", description="搜索姓名/电话/微信"),
status: str = Query("", description="状态筛选: pending/processed"),
db: Session = Depends(get_db),
):
query = db.query(CustomizeSubmission)
if keyword:
query = query.filter(
CustomizeSubmission.name.contains(keyword)
| CustomizeSubmission.phone.contains(keyword)
| CustomizeSubmission.wechat.contains(keyword)
)
if status in ("pending", "processed"):
query = query.filter(CustomizeSubmission.status == status)
total = query.count()
items = (
query.order_by(CustomizeSubmission.id.desc())
.offset((page - 1) * page_size)
.limit(page_size)
.all()
)
return CustomizeSubmissionListResponse(
items=[CustomizeSubmissionResponse.model_validate(i) for i in items],
total=total,
)
@router.get("/submissions/{submission_id}", response_model=CustomizeSubmissionResponse)
def get_submission(submission_id: int, db: Session = Depends(get_db)):
item = db.query(CustomizeSubmission).get(submission_id)
if not item:
raise HTTPException(status_code=404, detail="记录不存在")
return CustomizeSubmissionResponse.model_validate(item)
@router.put("/submissions/{submission_id}/status")
def update_status(
submission_id: int,
req: CustomizeStatusUpdate,
db: Session = Depends(get_db),
):
item = db.query(CustomizeSubmission).get(submission_id)
if not item:
raise HTTPException(status_code=404, detail="记录不存在")
item.status = req.status
db.commit()
return {"message": "状态更新成功"}
# --- Customize Config ---
@router.get("/config", response_model=CustomizeConfigSchema)
def get_config(db: Session = Depends(get_db)):
cfg = db.query(CustomizeConfig).first()
if not cfg:
return CustomizeConfigSchema()
return CustomizeConfigSchema(
trust_stats=cfg.trust_stats or [],
durations=cfg.durations or [],
activities=cfg.activities or [],
budgets=cfg.budgets or [],
process=cfg.process or [],
contact=cfg.contact or {},
)
@router.put("/config")
def update_config(req: CustomizeConfigSchema, db: Session = Depends(get_db)):
cfg = db.query(CustomizeConfig).first()
if not cfg:
cfg = CustomizeConfig()
db.add(cfg)
cfg.trust_stats = req.trust_stats
cfg.durations = req.durations
cfg.activities = req.activities
cfg.budgets = req.budgets
cfg.process = req.process
cfg.contact = req.contact
db.commit()
return {"message": "更新成功"}

118
app/routers/destinations.py 普通文件
查看文件

@ -0,0 +1,118 @@
from __future__ import annotations
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from app.database import get_db
from app.auth import get_current_user
from app.models.destination import DestinationConfig, DestinationItem, DestinationDimension, DestinationHonestItem
from app.schemas.destination import (
DestinationConfigSchema, DestinationItemSchema, DestinationItemResponse,
DestinationDimensionSchema, DestinationDimensionResponse,
DestinationHonestItemSchema, DestinationHonestItemResponse,
)
router = APIRouter(prefix="/api/destinations", tags=["目的地对比"], dependencies=[Depends(get_current_user)])
def _get_config(db: Session) -> DestinationConfig:
config = db.query(DestinationConfig).first()
if not config:
config = DestinationConfig()
db.add(config)
db.flush()
return config
@router.get("/config", response_model=DestinationConfigSchema)
def get_config(db: Session = Depends(get_db)):
c = _get_config(db)
return DestinationConfigSchema(
title=c.title, subtitle=c.subtitle, intro=c.intro,
closing_title=c.closing_title, closing_text=c.closing_text,
data_sources=c.data_sources, honest_title=c.honest_title, honest_subtitle=c.honest_subtitle,
)
@router.put("/config")
def update_config(req: DestinationConfigSchema, db: Session = Depends(get_db)):
c = _get_config(db)
c.title = req.title
c.subtitle = req.subtitle
c.intro = req.intro
c.closing_title = req.closing_title
c.closing_text = req.closing_text
c.data_sources = req.data_sources
c.honest_title = req.honest_title
c.honest_subtitle = req.honest_subtitle
db.commit()
return {"message": "更新成功"}
# --- Destination items ---
@router.get("/items", response_model=list[DestinationItemResponse])
def list_items(db: Session = Depends(get_db)):
rows = db.query(DestinationItem).order_by(DestinationItem.sort_order).all()
return [DestinationItemResponse(id=d.id, dest_id=d.dest_id, name=d.name, tag=d.tag, highlight=d.highlight, sort_order=d.sort_order) for d in rows]
@router.put("/items")
def update_items(items: list[DestinationItemSchema], db: Session = Depends(get_db)):
db.query(DestinationItem).delete()
for i, d in enumerate(items):
db.add(DestinationItem(dest_id=d.dest_id, name=d.name, tag=d.tag, highlight=d.highlight, sort_order=i))
db.commit()
return {"message": "目的地更新成功"}
# --- Dimensions ---
@router.get("/dimensions", response_model=list[DestinationDimensionResponse])
def list_dimensions(db: Session = Depends(get_db)):
rows = db.query(DestinationDimension).order_by(DestinationDimension.sort_order).all()
return [DestinationDimensionResponse(id=d.id, label=d.label, icon=d.icon, values=d.values or [], sort_order=d.sort_order) for d in rows]
@router.post("/dimensions", response_model=DestinationDimensionResponse, status_code=201)
def create_dimension(req: DestinationDimensionSchema, db: Session = Depends(get_db)):
d = DestinationDimension(label=req.label, icon=req.icon, values=req.values)
db.add(d)
db.commit()
db.refresh(d)
return DestinationDimensionResponse(id=d.id, label=d.label, icon=d.icon, values=d.values or [], sort_order=d.sort_order)
@router.put("/dimensions/{d_id}")
def update_dimension(d_id: int, req: DestinationDimensionSchema, db: Session = Depends(get_db)):
d = db.query(DestinationDimension).get(d_id)
if not d:
raise HTTPException(status_code=404, detail="对比维度不存在")
d.label = req.label
d.icon = req.icon
d.values = req.values
db.commit()
return {"message": "更新成功"}
@router.delete("/dimensions/{d_id}")
def delete_dimension(d_id: int, db: Session = Depends(get_db)):
d = db.query(DestinationDimension).get(d_id)
if not d:
raise HTTPException(status_code=404, detail="对比维度不存在")
db.delete(d)
db.commit()
return {"message": "删除成功"}
# --- Honest items ---
@router.get("/honest-items", response_model=list[DestinationHonestItemResponse])
def list_honest_items(db: Session = Depends(get_db)):
rows = db.query(DestinationHonestItem).order_by(DestinationHonestItem.sort_order).all()
return [DestinationHonestItemResponse(id=h.id, text=h.text, sort_order=h.sort_order) for h in rows]
@router.put("/honest-items")
def update_honest_items(items: list[DestinationHonestItemSchema], db: Session = Depends(get_db)):
db.query(DestinationHonestItem).delete()
for i, h in enumerate(items):
db.add(DestinationHonestItem(text=h.text, sort_order=i))
db.commit()
return {"message": "坦诚说明更新成功"}

查看文件

@ -0,0 +1,92 @@
from __future__ import annotations
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session
from sqlalchemy import func as sqlfunc
from app.database import get_db
from app.auth import get_current_user
from app.models.destination_detail import DestinationDetail
from app.schemas.destination_detail import (
DestinationDetailSchema,
DestinationDetailResponse,
DestinationDetailListResponse,
)
router = APIRouter(
prefix="/api/destinations-detail",
tags=["景点详情"],
dependencies=[Depends(get_current_user)],
)
@router.get("/", response_model=DestinationDetailListResponse)
def list_items(
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=100),
keyword: str = Query("", description="搜索关键词"),
db: Session = Depends(get_db),
):
query = db.query(DestinationDetail)
if keyword:
query = query.filter(
DestinationDetail.name.contains(keyword)
| DestinationDetail.location.contains(keyword)
)
total = query.count()
items = (
query.order_by(DestinationDetail.sort_order, DestinationDetail.id.desc())
.offset((page - 1) * page_size)
.limit(page_size)
.all()
)
return DestinationDetailListResponse(
items=[DestinationDetailResponse.model_validate(i) for i in items],
total=total,
)
@router.post("/", response_model=DestinationDetailResponse, status_code=201)
def create_item(req: DestinationDetailSchema, db: Session = Depends(get_db)):
if db.query(DestinationDetail).filter(DestinationDetail.slug == req.slug).first():
raise HTTPException(status_code=400, detail="该 slug 已存在")
max_order = db.query(sqlfunc.max(DestinationDetail.sort_order)).scalar() or 0
item = DestinationDetail(**req.model_dump(), sort_order=max_order + 1)
db.add(item)
db.commit()
db.refresh(item)
return DestinationDetailResponse.model_validate(item)
@router.put("/{item_id}", response_model=DestinationDetailResponse)
def update_item(item_id: int, req: DestinationDetailSchema, db: Session = Depends(get_db)):
item = db.query(DestinationDetail).get(item_id)
if not item:
raise HTTPException(status_code=404, detail="景点不存在")
existing = db.query(DestinationDetail).filter(
DestinationDetail.slug == req.slug, DestinationDetail.id != item_id
).first()
if existing:
raise HTTPException(status_code=400, detail="该 slug 已被其他景点使用")
for key, val in req.model_dump().items():
setattr(item, key, val)
db.commit()
db.refresh(item)
return DestinationDetailResponse.model_validate(item)
@router.delete("/{item_id}")
def delete_item(item_id: int, db: Session = Depends(get_db)):
item = db.query(DestinationDetail).get(item_id)
if not item:
raise HTTPException(status_code=404, detail="景点不存在")
db.delete(item)
db.commit()
return {"message": "删除成功"}
@router.put("/reorder/batch")
def reorder_items(ids: list[int], db: Session = Depends(get_db)):
for i, iid in enumerate(ids):
db.query(DestinationDetail).filter(DestinationDetail.id == iid).update({"sort_order": i})
db.commit()
return {"message": "排序更新成功"}

60
app/routers/export.py 普通文件
查看文件

@ -0,0 +1,60 @@
from __future__ import annotations
import subprocess
from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from app.database import get_db
from app.auth import get_current_user
from app.config import settings
from app.models.user import AdminUser
from app.models.export_log import ExportLog
from app.schemas.export import ExportRequest, ExportLogResponse
from app.services.export_service import export_modules
router = APIRouter(prefix="/api/export", tags=["导出"], dependencies=[Depends(get_current_user)])
@router.post("/")
def do_export(req: ExportRequest, db: Session = Depends(get_db), current_user: AdminUser = Depends(get_current_user)):
results = export_modules(db, req.modules)
all_success = all(v == "success" for v in results.values())
# Optionally trigger rebuild
rebuild_msg = ""
if req.trigger_rebuild and all_success:
try:
subprocess.run(
["npx", "nuxi", "generate"],
cwd=settings.NUXT_PROJECT_PATH,
timeout=300,
capture_output=True,
)
rebuild_msg = " | 静态站已重新构建"
except Exception as e:
rebuild_msg = f" | 构建失败: {str(e)}"
# Log
log = ExportLog(
exported_by=current_user.username,
modules=req.modules if "all" not in req.modules else ["all"],
status="success" if all_success else "failed",
message=str(results) + rebuild_msg,
)
db.add(log)
db.commit()
return {"results": results, "rebuild": rebuild_msg}
@router.get("/log", response_model=list[ExportLogResponse])
def get_export_log(db: Session = Depends(get_db)):
logs = db.query(ExportLog).order_by(ExportLog.created_at.desc()).limit(50).all()
return [
ExportLogResponse(
id=l.id, exported_by=l.exported_by, modules=l.modules,
status=l.status, message=l.message,
created_at=l.created_at.isoformat() if l.created_at else None,
)
for l in logs
]

90
app/routers/faq.py 普通文件
查看文件

@ -0,0 +1,90 @@
from __future__ import annotations
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from sqlalchemy import func as sqlfunc
from app.database import get_db
from app.auth import get_current_user
from app.models.faq import FaqCategory, FaqQuestion
from app.schemas.faq import FaqCategorySchema, FaqCategoryResponse, FaqQuestionSchema, FaqQuestionResponse
router = APIRouter(prefix="/api/faq", tags=["常见问答"], dependencies=[Depends(get_current_user)])
@router.get("/categories", response_model=list[FaqCategoryResponse])
def list_categories(db: Session = Depends(get_db)):
cats = db.query(FaqCategory).order_by(FaqCategory.sort_order).all()
return [FaqCategoryResponse.model_validate(c) for c in cats]
@router.post("/categories", response_model=FaqCategoryResponse, status_code=201)
def create_category(req: FaqCategorySchema, db: Session = Depends(get_db)):
max_order = db.query(sqlfunc.max(FaqCategory.sort_order)).scalar() or 0
cat = FaqCategory(category_id=req.category_id, name=req.name, sort_order=max_order + 1)
db.add(cat)
db.commit()
db.refresh(cat)
return FaqCategoryResponse.model_validate(cat)
@router.put("/categories/{cat_id}")
def update_category(cat_id: int, req: FaqCategorySchema, db: Session = Depends(get_db)):
cat = db.query(FaqCategory).get(cat_id)
if not cat:
raise HTTPException(status_code=404, detail="分类不存在")
cat.category_id = req.category_id
cat.name = req.name
db.commit()
return {"message": "更新成功"}
@router.delete("/categories/{cat_id}")
def delete_category(cat_id: int, db: Session = Depends(get_db)):
cat = db.query(FaqCategory).get(cat_id)
if not cat:
raise HTTPException(status_code=404, detail="分类不存在")
db.delete(cat)
db.commit()
return {"message": "删除成功"}
@router.post("/questions", response_model=FaqQuestionResponse, status_code=201)
def create_question(req: FaqQuestionSchema, category_id: int = None, db: Session = Depends(get_db)):
if not category_id:
raise HTTPException(status_code=400, detail="必须指定分类")
max_order = db.query(sqlfunc.max(FaqQuestion.sort_order)).filter(FaqQuestion.category_id == category_id).scalar() or 0
q = FaqQuestion(
category_id=category_id,
question_id=req.question_id,
question=req.question,
answer=req.answer,
related_links=[link.model_dump() for link in req.related_links],
sort_order=max_order + 1,
)
db.add(q)
db.commit()
db.refresh(q)
return FaqQuestionResponse.model_validate(q)
@router.put("/questions/{q_id}")
def update_question(q_id: int, req: FaqQuestionSchema, db: Session = Depends(get_db)):
q = db.query(FaqQuestion).get(q_id)
if not q:
raise HTTPException(status_code=404, detail="问题不存在")
q.question_id = req.question_id
q.question = req.question
q.answer = req.answer
q.related_links = [link.model_dump() for link in req.related_links]
db.commit()
return {"message": "更新成功"}
@router.delete("/questions/{q_id}")
def delete_question(q_id: int, db: Session = Depends(get_db)):
q = db.query(FaqQuestion).get(q_id)
if not q:
raise HTTPException(status_code=404, detail="问题不存在")
db.delete(q)
db.commit()
return {"message": "删除成功"}

82
app/routers/gallery.py 普通文件
查看文件

@ -0,0 +1,82 @@
from __future__ import annotations
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session
from sqlalchemy import func as sqlfunc
from app.database import get_db
from app.auth import get_current_user
from app.models.gallery import GalleryItem
from app.schemas.gallery import GalleryItemSchema, GalleryItemResponse, GalleryListResponse
router = APIRouter(
prefix="/api/gallery",
tags=["旅拍作品"],
dependencies=[Depends(get_current_user)],
)
@router.get("/", response_model=GalleryListResponse)
def list_items(
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=100),
keyword: str = Query("", description="搜索关键词"),
db: Session = Depends(get_db),
):
query = db.query(GalleryItem)
if keyword:
query = query.filter(
GalleryItem.title.contains(keyword)
| GalleryItem.location.contains(keyword)
| GalleryItem.photographer.contains(keyword)
)
total = query.count()
items = (
query.order_by(GalleryItem.sort_order, GalleryItem.id.desc())
.offset((page - 1) * page_size)
.limit(page_size)
.all()
)
return GalleryListResponse(
items=[GalleryItemResponse.model_validate(i) for i in items],
total=total,
)
@router.post("/", response_model=GalleryItemResponse, status_code=201)
def create_item(req: GalleryItemSchema, db: Session = Depends(get_db)):
max_order = db.query(sqlfunc.max(GalleryItem.sort_order)).scalar() or 0
item = GalleryItem(**req.model_dump(), sort_order=max_order + 1)
db.add(item)
db.commit()
db.refresh(item)
return GalleryItemResponse.model_validate(item)
@router.put("/{item_id}", response_model=GalleryItemResponse)
def update_item(item_id: int, req: GalleryItemSchema, db: Session = Depends(get_db)):
item = db.query(GalleryItem).get(item_id)
if not item:
raise HTTPException(status_code=404, detail="作品不存在")
for key, val in req.model_dump().items():
setattr(item, key, val)
db.commit()
db.refresh(item)
return GalleryItemResponse.model_validate(item)
@router.delete("/{item_id}")
def delete_item(item_id: int, db: Session = Depends(get_db)):
item = db.query(GalleryItem).get(item_id)
if not item:
raise HTTPException(status_code=404, detail="作品不存在")
db.delete(item)
db.commit()
return {"message": "删除成功"}
@router.put("/reorder/batch")
def reorder_items(ids: list[int], db: Session = Depends(get_db)):
for i, iid in enumerate(ids):
db.query(GalleryItem).filter(GalleryItem.id == iid).update({"sort_order": i})
db.commit()
return {"message": "排序更新成功"}

55
app/routers/guides.py 普通文件
查看文件

@ -0,0 +1,55 @@
from __future__ import annotations
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from app.database import get_db
from app.auth import get_current_user
from app.models.guide import GuideConfig, GuideSection
from app.schemas.guide import GuideConfigSchema, GuideSectionSchema, GuideSectionResponse
router = APIRouter(prefix="/api/guides", tags=["出行指南"], dependencies=[Depends(get_current_user)])
def _get_config(db: Session) -> GuideConfig:
config = db.query(GuideConfig).first()
if not config:
config = GuideConfig()
db.add(config)
db.flush()
return config
@router.get("/config", response_model=GuideConfigSchema)
def get_config(db: Session = Depends(get_db)):
c = _get_config(db)
return GuideConfigSchema(page_intro=c.page_intro)
@router.put("/config")
def update_config(req: GuideConfigSchema, db: Session = Depends(get_db)):
c = _get_config(db)
c.page_intro = req.page_intro
db.commit()
return {"message": "更新成功"}
@router.get("/sections", response_model=list[GuideSectionResponse])
def list_sections(db: Session = Depends(get_db)):
c = _get_config(db)
sections = db.query(GuideSection).filter_by(config_id=c.id).order_by(GuideSection.sort_order).all()
return [GuideSectionResponse.model_validate(s) for s in sections]
@router.put("/sections/{section_id}")
def update_section(section_id: str, req: GuideSectionSchema, db: Session = Depends(get_db)):
c = _get_config(db)
section = db.query(GuideSection).filter_by(config_id=c.id, section_id=section_id).first()
if not section:
raise HTTPException(status_code=404, detail=f"章节 {section_id} 不存在")
section.title = req.title
section.subtitle = req.subtitle
section.icon = req.icon
section.content = req.content
section.data = req.data
db.commit()
return {"message": "更新成功"}

44
app/routers/navigation.py 普通文件
查看文件

@ -0,0 +1,44 @@
from __future__ import annotations
from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from app.database import get_db
from app.auth import get_current_user
from app.models.navigation import NavHeader, NavFooterGroup, NavFooterLink
from app.schemas.navigation import NavigationSchema
router = APIRouter(prefix="/api/navigation", tags=["导航菜单"], dependencies=[Depends(get_current_user)])
@router.get("/", response_model=NavigationSchema)
def get_navigation(db: Session = Depends(get_db)):
headers = db.query(NavHeader).order_by(NavHeader.sort_order).all()
groups = db.query(NavFooterGroup).order_by(NavFooterGroup.sort_order).all()
return NavigationSchema(
header=[{"text": h.text, "to": h.to_path} for h in headers],
footer=[
{"title": g.title, "links": [{"text": l.text, "to": l.to_path} for l in g.links]}
for g in groups
],
)
@router.put("/")
def update_navigation(req: NavigationSchema, db: Session = Depends(get_db)):
# Replace header
db.query(NavHeader).delete()
for i, h in enumerate(req.header):
db.add(NavHeader(text=h.text, to_path=h.to, sort_order=i))
# Replace footer
db.query(NavFooterLink).delete()
db.query(NavFooterGroup).delete()
for i, g in enumerate(req.footer):
group = NavFooterGroup(title=g.title, sort_order=i)
db.add(group)
db.flush()
for j, link in enumerate(g.links):
db.add(NavFooterLink(group_id=group.id, text=link.text, to_path=link.to, sort_order=j))
db.commit()
return {"message": "导航更新成功"}

69
app/routers/news.py 普通文件
查看文件

@ -0,0 +1,69 @@
from __future__ import annotations
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session
from sqlalchemy import func as sqlfunc
from app.database import get_db
from app.auth import get_current_user
from app.models.news import News
from app.schemas.news import NewsSchema, NewsResponse, NewsListResponse
router = APIRouter(prefix="/api/news", tags=["新闻动态"], dependencies=[Depends(get_current_user)])
@router.get("/", response_model=NewsListResponse)
def list_news(
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=100),
keyword: str = Query("", description="搜索关键词"),
db: Session = Depends(get_db),
):
query = db.query(News)
if keyword:
query = query.filter(News.title.contains(keyword) | News.summary.contains(keyword))
total = query.count()
items = query.order_by(News.sort_order, News.id.desc()).offset((page - 1) * page_size).limit(page_size).all()
return NewsListResponse(
items=[NewsResponse.model_validate(n) for n in items],
total=total,
)
@router.post("/", response_model=NewsResponse, status_code=201)
def create_news(req: NewsSchema, db: Session = Depends(get_db)):
max_order = db.query(sqlfunc.max(News.sort_order)).scalar() or 0
news = News(**req.model_dump(), sort_order=max_order + 1)
db.add(news)
db.commit()
db.refresh(news)
return NewsResponse.model_validate(news)
@router.put("/{news_id}", response_model=NewsResponse)
def update_news(news_id: int, req: NewsSchema, db: Session = Depends(get_db)):
news = db.query(News).get(news_id)
if not news:
raise HTTPException(status_code=404, detail="新闻不存在")
for key, val in req.model_dump().items():
setattr(news, key, val)
db.commit()
db.refresh(news)
return NewsResponse.model_validate(news)
@router.delete("/{news_id}")
def delete_news(news_id: int, db: Session = Depends(get_db)):
news = db.query(News).get(news_id)
if not news:
raise HTTPException(status_code=404, detail="新闻不存在")
db.delete(news)
db.commit()
return {"message": "删除成功"}
@router.put("/reorder/batch")
def reorder_news(ids: list[int], db: Session = Depends(get_db)):
for i, nid in enumerate(ids):
db.query(News).filter(News.id == nid).update({"sort_order": i})
db.commit()
return {"message": "排序更新成功"}

57
app/routers/partners.py 普通文件
查看文件

@ -0,0 +1,57 @@
from __future__ import annotations
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from sqlalchemy import func as sqlfunc
from app.database import get_db
from app.auth import get_current_user
from app.models.partner import Partner
from app.schemas.partner import PartnerSchema, PartnerResponse
router = APIRouter(prefix="/api/partners", tags=["合作伙伴"], dependencies=[Depends(get_current_user)])
@router.get("/", response_model=list[PartnerResponse])
def list_partners(db: Session = Depends(get_db)):
items = db.query(Partner).order_by(Partner.sort_order).all()
return [PartnerResponse.model_validate(item) for item in items]
@router.post("/", response_model=PartnerResponse, status_code=201)
def create_partner(req: PartnerSchema, db: Session = Depends(get_db)):
max_order = db.query(sqlfunc.max(Partner.sort_order)).scalar() or 0
item = Partner(**req.model_dump(), sort_order=max_order + 1)
db.add(item)
db.commit()
db.refresh(item)
return PartnerResponse.model_validate(item)
@router.put("/{item_id}", response_model=PartnerResponse)
def update_partner(item_id: int, req: PartnerSchema, db: Session = Depends(get_db)):
item = db.query(Partner).get(item_id)
if not item:
raise HTTPException(status_code=404, detail="合作伙伴不存在")
for key, val in req.model_dump().items():
setattr(item, key, val)
db.commit()
db.refresh(item)
return PartnerResponse.model_validate(item)
@router.delete("/{item_id}")
def delete_partner(item_id: int, db: Session = Depends(get_db)):
item = db.query(Partner).get(item_id)
if not item:
raise HTTPException(status_code=404, detail="合作伙伴不存在")
db.delete(item)
db.commit()
return {"message": "删除成功"}
@router.put("/reorder/batch")
def reorder_partners(ids: list[int], db: Session = Depends(get_db)):
for i, pid in enumerate(ids):
db.query(Partner).filter(Partner.id == pid).update({"sort_order": i})
db.commit()
return {"message": "排序更新成功"}

57
app/routers/pricing.py 普通文件
查看文件

@ -0,0 +1,57 @@
from __future__ import annotations
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from sqlalchemy import func as sqlfunc
from app.database import get_db
from app.auth import get_current_user
from app.models.pricing import PricingItem
from app.schemas.pricing import PricingItemSchema, PricingItemResponse
router = APIRouter(prefix="/api/pricing", tags=["价格信息"], dependencies=[Depends(get_current_user)])
@router.get("/", response_model=list[PricingItemResponse])
def list_pricing(db: Session = Depends(get_db)):
items = db.query(PricingItem).order_by(PricingItem.sort_order).all()
return [PricingItemResponse.model_validate(item) for item in items]
@router.post("/", response_model=PricingItemResponse, status_code=201)
def create_pricing(req: PricingItemSchema, db: Session = Depends(get_db)):
max_order = db.query(sqlfunc.max(PricingItem.sort_order)).scalar() or 0
item = PricingItem(**req.model_dump(), sort_order=max_order + 1)
db.add(item)
db.commit()
db.refresh(item)
return PricingItemResponse.model_validate(item)
@router.put("/{item_id}", response_model=PricingItemResponse)
def update_pricing(item_id: int, req: PricingItemSchema, db: Session = Depends(get_db)):
item = db.query(PricingItem).get(item_id)
if not item:
raise HTTPException(status_code=404, detail="价格信息不存在")
for key, val in req.model_dump().items():
setattr(item, key, val)
db.commit()
db.refresh(item)
return PricingItemResponse.model_validate(item)
@router.delete("/{item_id}")
def delete_pricing(item_id: int, db: Session = Depends(get_db)):
item = db.query(PricingItem).get(item_id)
if not item:
raise HTTPException(status_code=404, detail="价格信息不存在")
db.delete(item)
db.commit()
return {"message": "删除成功"}
@router.put("/reorder/batch")
def reorder_pricing(ids: list[int], db: Session = Depends(get_db)):
for i, pid in enumerate(ids):
db.query(PricingItem).filter(PricingItem.id == pid).update({"sort_order": i})
db.commit()
return {"message": "排序更新成功"}

187
app/routers/products.py 普通文件
查看文件

@ -0,0 +1,187 @@
from __future__ import annotations
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from app.database import get_db
from app.auth import get_current_user
from app.models.product import (
ProductConfig, ProductVersion, ProductVersionHighlight,
SummerCamp, SummerCampPrinciple, SummerCampActivity, SummerCampItinerary, SummerCampFaq,
SelectionGuide,
)
from app.schemas.product import (
ProductConfigSchema, ProductVersionSchema, ProductVersionResponse,
SummerCampSchema, SelectionGuideSchema,
)
router = APIRouter(prefix="/api/products", tags=["产品管理"], dependencies=[Depends(get_current_user)])
def _get_config(db: Session) -> ProductConfig:
config = db.query(ProductConfig).first()
if not config:
config = ProductConfig()
db.add(config)
db.flush()
return config
@router.get("/config", response_model=ProductConfigSchema)
def get_config(db: Session = Depends(get_db)):
config = _get_config(db)
return ProductConfigSchema(narrative=config.narrative, pricing_philosophy=config.pricing_philosophy)
@router.put("/config")
def update_config(req: ProductConfigSchema, db: Session = Depends(get_db)):
config = _get_config(db)
config.narrative = req.narrative
config.pricing_philosophy = req.pricing_philosophy
db.commit()
return {"message": "更新成功"}
@router.get("/versions", response_model=list[ProductVersionResponse])
def list_versions(db: Session = Depends(get_db)):
config = _get_config(db)
versions = db.query(ProductVersion).filter_by(config_id=config.id).order_by(ProductVersion.sort_order).all()
result = []
for v in versions:
resp = ProductVersionResponse(
id=v.id, version_id=v.version_id, name=v.name, days=v.days, nights=v.nights,
audience=v.audience, description=v.description, tag=v.tag, sort_order=v.sort_order,
highlights=[h.text for h in v.highlights],
)
result.append(resp)
return result
@router.post("/versions", response_model=ProductVersionResponse, status_code=201)
def create_version(req: ProductVersionSchema, db: Session = Depends(get_db)):
config = _get_config(db)
v = ProductVersion(
config_id=config.id, version_id=req.version_id, name=req.name,
days=req.days, nights=req.nights, audience=req.audience,
description=req.description, tag=req.tag,
)
db.add(v)
db.flush()
for i, h in enumerate(req.highlights):
db.add(ProductVersionHighlight(version_id=v.id, text=h, sort_order=i))
db.commit()
db.refresh(v)
return ProductVersionResponse(
id=v.id, version_id=v.version_id, name=v.name, days=v.days, nights=v.nights,
audience=v.audience, description=v.description, tag=v.tag, sort_order=v.sort_order,
highlights=[h.text for h in v.highlights],
)
@router.put("/versions/{v_id}")
def update_version(v_id: int, req: ProductVersionSchema, db: Session = Depends(get_db)):
v = db.query(ProductVersion).get(v_id)
if not v:
raise HTTPException(status_code=404, detail="版本不存在")
v.version_id = req.version_id
v.name = req.name
v.days = req.days
v.nights = req.nights
v.audience = req.audience
v.description = req.description
v.tag = req.tag
# Replace highlights
db.query(ProductVersionHighlight).filter_by(version_id=v.id).delete()
for i, h in enumerate(req.highlights):
db.add(ProductVersionHighlight(version_id=v.id, text=h, sort_order=i))
db.commit()
return {"message": "更新成功"}
@router.delete("/versions/{v_id}")
def delete_version(v_id: int, db: Session = Depends(get_db)):
v = db.query(ProductVersion).get(v_id)
if not v:
raise HTTPException(status_code=404, detail="版本不存在")
db.delete(v)
db.commit()
return {"message": "删除成功"}
@router.get("/summer-camp", response_model=SummerCampSchema)
def get_summer_camp(db: Session = Depends(get_db)):
config = _get_config(db)
camp = db.query(SummerCamp).filter_by(config_id=config.id).first()
if not camp:
return SummerCampSchema()
return SummerCampSchema(
name=camp.name, positioning=camp.positioning, days=camp.days, nights=camp.nights,
sessions=camp.sessions_json, difference_from_v9=camp.difference_from_v9,
principles=[p.text for p in camp.principles],
activities=[a.text for a in camp.activities],
itinerary=[it.text for it in camp.itinerary],
faq=[{"question": f.question, "answer": f.answer} for f in camp.faq],
)
@router.put("/summer-camp")
def update_summer_camp(req: SummerCampSchema, db: Session = Depends(get_db)):
config = _get_config(db)
camp = db.query(SummerCamp).filter_by(config_id=config.id).first()
if not camp:
camp = SummerCamp(config_id=config.id)
db.add(camp)
db.flush()
camp.name = req.name
camp.positioning = req.positioning
camp.days = req.days
camp.nights = req.nights
camp.sessions_json = req.sessions
camp.difference_from_v9 = req.difference_from_v9
# Replace sub-collections
db.query(SummerCampPrinciple).filter_by(camp_id=camp.id).delete()
for i, p in enumerate(req.principles):
db.add(SummerCampPrinciple(camp_id=camp.id, text=p, sort_order=i))
db.query(SummerCampActivity).filter_by(camp_id=camp.id).delete()
for i, a in enumerate(req.activities):
db.add(SummerCampActivity(camp_id=camp.id, text=a, sort_order=i))
db.query(SummerCampItinerary).filter_by(camp_id=camp.id).delete()
for i, it in enumerate(req.itinerary):
db.add(SummerCampItinerary(camp_id=camp.id, text=it, sort_order=i))
db.query(SummerCampFaq).filter_by(camp_id=camp.id).delete()
for i, f in enumerate(req.faq):
db.add(SummerCampFaq(camp_id=camp.id, question=f.question, answer=f.answer, sort_order=i))
db.commit()
return {"message": "夏令营更新成功"}
@router.get("/selection-guide", response_model=SelectionGuideSchema)
def get_selection_guide(db: Session = Depends(get_db)):
config = _get_config(db)
guide = db.query(SelectionGuide).filter_by(config_id=config.id).first()
if not guide:
return SelectionGuideSchema()
return SelectionGuideSchema(
by_vacation_length=guide.by_vacation_length or [],
by_child_age=guide.by_child_age or [],
by_preference=guide.by_preference or [],
)
@router.put("/selection-guide")
def update_selection_guide(req: SelectionGuideSchema, db: Session = Depends(get_db)):
config = _get_config(db)
guide = db.query(SelectionGuide).filter_by(config_id=config.id).first()
if not guide:
guide = SelectionGuide(config_id=config.id)
db.add(guide)
guide.by_vacation_length = req.by_vacation_length
guide.by_child_age = req.by_child_age
guide.by_preference = req.by_preference
db.commit()
return {"message": "选品指南更新成功"}

查看文件

@ -0,0 +1,57 @@
from __future__ import annotations
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from sqlalchemy import func as sqlfunc
from app.database import get_db
from app.auth import get_current_user
from app.models.qualification import Qualification
from app.schemas.qualification import QualificationSchema, QualificationResponse
router = APIRouter(prefix="/api/qualifications", tags=["资质荣誉"], dependencies=[Depends(get_current_user)])
@router.get("/", response_model=list[QualificationResponse])
def list_qualifications(db: Session = Depends(get_db)):
items = db.query(Qualification).order_by(Qualification.sort_order).all()
return [QualificationResponse.model_validate(item) for item in items]
@router.post("/", response_model=QualificationResponse, status_code=201)
def create_qualification(req: QualificationSchema, db: Session = Depends(get_db)):
max_order = db.query(sqlfunc.max(Qualification.sort_order)).scalar() or 0
item = Qualification(**req.model_dump(), sort_order=max_order + 1)
db.add(item)
db.commit()
db.refresh(item)
return QualificationResponse.model_validate(item)
@router.put("/{item_id}", response_model=QualificationResponse)
def update_qualification(item_id: int, req: QualificationSchema, db: Session = Depends(get_db)):
item = db.query(Qualification).get(item_id)
if not item:
raise HTTPException(status_code=404, detail="资质信息不存在")
for key, val in req.model_dump().items():
setattr(item, key, val)
db.commit()
db.refresh(item)
return QualificationResponse.model_validate(item)
@router.delete("/{item_id}")
def delete_qualification(item_id: int, db: Session = Depends(get_db)):
item = db.query(Qualification).get(item_id)
if not item:
raise HTTPException(status_code=404, detail="资质信息不存在")
db.delete(item)
db.commit()
return {"message": "删除成功"}
@router.put("/reorder/batch")
def reorder_qualifications(ids: list[int], db: Session = Depends(get_db)):
for i, qid in enumerate(ids):
db.query(Qualification).filter(Qualification.id == qid).update({"sort_order": i})
db.commit()
return {"message": "排序更新成功"}

100
app/routers/reviews.py 普通文件
查看文件

@ -0,0 +1,100 @@
from __future__ import annotations
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session
from sqlalchemy import func as sqlfunc
from app.database import get_db
from app.auth import get_current_user
from app.models.review import Review, ReviewSummary
from app.schemas.review import ReviewSchema, ReviewResponse, ReviewListResponse, ReviewSummarySchema
router = APIRouter(prefix="/api/reviews", tags=["客户评价"], dependencies=[Depends(get_current_user)])
@router.get("/summary", response_model=ReviewSummarySchema)
def get_summary(db: Session = Depends(get_db)):
summary = db.query(ReviewSummary).first()
if not summary:
return ReviewSummarySchema()
return ReviewSummarySchema(total_count=summary.total_count, approval_rate=summary.approval_rate, keywords=summary.keywords or [])
@router.put("/summary")
def update_summary(req: ReviewSummarySchema, db: Session = Depends(get_db)):
summary = db.query(ReviewSummary).first()
if not summary:
summary = ReviewSummary()
db.add(summary)
summary.total_count = req.total_count
summary.approval_rate = req.approval_rate
summary.keywords = req.keywords
db.commit()
return {"message": "评价概要更新成功"}
@router.get("/", response_model=ReviewListResponse)
def list_reviews(
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=100),
keyword: str = Query("", description="搜索关键词"),
db: Session = Depends(get_db),
):
query = db.query(Review)
if keyword:
query = query.filter(Review.nickname.contains(keyword) | Review.content.contains(keyword))
total = query.count()
items = query.order_by(Review.sort_order, Review.id).offset((page - 1) * page_size).limit(page_size).all()
return ReviewListResponse(
items=[ReviewResponse.model_validate(r) for r in items],
total=total,
)
@router.post("/", response_model=ReviewResponse, status_code=201)
def create_review(req: ReviewSchema, db: Session = Depends(get_db)):
max_order = db.query(sqlfunc.max(Review.sort_order)).scalar() or 0
review = Review(
nickname=req.nickname,
travel_date=req.travel_date,
product_version=req.product_version,
screenshot=req.screenshot,
content=req.content,
scenes=req.scenes,
concerns=req.concerns,
is_visible=req.is_visible,
sort_order=max_order + 1,
)
db.add(review)
db.commit()
db.refresh(review)
return ReviewResponse.model_validate(review)
@router.put("/{review_id}", response_model=ReviewResponse)
def update_review(review_id: int, req: ReviewSchema, db: Session = Depends(get_db)):
review = db.query(Review).get(review_id)
if not review:
raise HTTPException(status_code=404, detail="评价不存在")
for key, val in req.model_dump().items():
setattr(review, key, val)
db.commit()
db.refresh(review)
return ReviewResponse.model_validate(review)
@router.delete("/{review_id}")
def delete_review(review_id: int, db: Session = Depends(get_db)):
review = db.query(Review).get(review_id)
if not review:
raise HTTPException(status_code=404, detail="评价不存在")
db.delete(review)
db.commit()
return {"message": "删除成功"}
@router.put("/reorder/batch")
def reorder_reviews(ids: list[int], db: Session = Depends(get_db)):
for i, rid in enumerate(ids):
db.query(Review).filter(Review.id == rid).update({"sort_order": i})
db.commit()
return {"message": "排序更新成功"}

查看文件

@ -0,0 +1,174 @@
"""
Seasonal products router: serves both autumn (游牧的森林) and winter (嗨冰雪).
Mounted twice at /api/autumn-products and /api/winter-products.
"""
from __future__ import annotations
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from app.database import get_db
from app.auth import get_current_user
from app.models.seasonal_product import (
SeasonalProductConfig, SeasonalProductVersion,
SeasonalProductHighlight, SeasonalProductTimeline,
)
from app.schemas.seasonal_product import (
SeasonalConfigSchema, SeasonalVersionSchema, SeasonalVersionResponse,
SeasonalHighlightSchema, SeasonalHighlightResponse,
SeasonalTimelineSchema, SeasonalTimelineResponse,
)
def create_seasonal_router(season: str, label: str) -> APIRouter:
router = APIRouter(prefix=f"/api/{season}-products", tags=[f"{label}产品"], dependencies=[Depends(get_current_user)])
def _get_config(db: Session) -> SeasonalProductConfig:
config = db.query(SeasonalProductConfig).filter_by(season=season).first()
if not config:
config = SeasonalProductConfig(season=season)
db.add(config)
db.flush()
return config
@router.get("/config", response_model=SeasonalConfigSchema)
def get_config(db: Session = Depends(get_db)):
c = _get_config(db)
return SeasonalConfigSchema(
narrative=c.narrative, style=c.style, brand_name=c.brand_name,
version_label=c.version_label, season_label=c.season_label,
)
@router.put("/config")
def update_config(req: SeasonalConfigSchema, db: Session = Depends(get_db)):
c = _get_config(db)
c.narrative = req.narrative
c.style = req.style
c.brand_name = req.brand_name
c.version_label = req.version_label
c.season_label = req.season_label
db.commit()
return {"message": "更新成功"}
# --- Versions ---
@router.get("/versions", response_model=list[SeasonalVersionResponse])
def list_versions(db: Session = Depends(get_db)):
rows = db.query(SeasonalProductVersion).filter_by(season=season).order_by(SeasonalProductVersion.sort_order).all()
return [
SeasonalVersionResponse(
id=v.id, version_id=v.version_id, name=v.name, days=v.days, nights=v.nights,
tag=v.tag, line=v.line, route=v.route, audience=v.audience,
description=v.description, highlights=v.highlights or [], itinerary=v.itinerary or [],
sort_order=v.sort_order,
)
for v in rows
]
@router.post("/versions", response_model=SeasonalVersionResponse, status_code=201)
def create_version(req: SeasonalVersionSchema, db: Session = Depends(get_db)):
v = SeasonalProductVersion(
season=season, version_id=req.version_id, name=req.name,
days=req.days, nights=req.nights, tag=req.tag, line=req.line,
route=req.route, audience=req.audience, description=req.description,
highlights=req.highlights, itinerary=req.itinerary,
)
db.add(v)
db.commit()
db.refresh(v)
return SeasonalVersionResponse(
id=v.id, version_id=v.version_id, name=v.name, days=v.days, nights=v.nights,
tag=v.tag, line=v.line, route=v.route, audience=v.audience,
description=v.description, highlights=v.highlights or [], itinerary=v.itinerary or [],
sort_order=v.sort_order,
)
@router.put("/versions/{v_id}")
def update_version(v_id: int, req: SeasonalVersionSchema, db: Session = Depends(get_db)):
v = db.query(SeasonalProductVersion).get(v_id)
if not v or v.season != season:
raise HTTPException(status_code=404, detail="版本不存在")
v.version_id = req.version_id
v.name = req.name
v.days = req.days
v.nights = req.nights
v.tag = req.tag
v.line = req.line
v.route = req.route
v.audience = req.audience
v.description = req.description
v.highlights = req.highlights
v.itinerary = req.itinerary
db.commit()
return {"message": "更新成功"}
@router.delete("/versions/{v_id}")
def delete_version(v_id: int, db: Session = Depends(get_db)):
v = db.query(SeasonalProductVersion).get(v_id)
if not v or v.season != season:
raise HTTPException(status_code=404, detail="版本不存在")
db.delete(v)
db.commit()
return {"message": "删除成功"}
# --- Highlights ---
@router.get("/highlights", response_model=list[SeasonalHighlightResponse])
def list_highlights(category: str = None, db: Session = Depends(get_db)):
q = db.query(SeasonalProductHighlight).filter_by(season=season)
if category:
q = q.filter_by(category=category)
return [
SeasonalHighlightResponse(id=h.id, category=h.category, title=h.title, description=h.description, sort_order=h.sort_order)
for h in q.order_by(SeasonalProductHighlight.sort_order).all()
]
@router.put("/highlights")
def update_highlights(items: list[SeasonalHighlightSchema], db: Session = Depends(get_db)):
db.query(SeasonalProductHighlight).filter_by(season=season).delete()
for i, h in enumerate(items):
db.add(SeasonalProductHighlight(season=season, category=h.category, title=h.title, description=h.description, sort_order=i))
db.commit()
return {"message": "亮点更新成功"}
# --- Timeline ---
@router.get("/timeline", response_model=list[SeasonalTimelineResponse])
def list_timeline(db: Session = Depends(get_db)):
rows = db.query(SeasonalProductTimeline).filter_by(season=season).order_by(SeasonalProductTimeline.sort_order).all()
return [
SeasonalTimelineResponse(id=t.id, version=t.version, date=t.date, title=t.title, changes=t.changes or [], reason=t.reason, sort_order=t.sort_order)
for t in rows
]
@router.post("/timeline", response_model=SeasonalTimelineResponse, status_code=201)
def create_timeline(req: SeasonalTimelineSchema, db: Session = Depends(get_db)):
t = SeasonalProductTimeline(season=season, version=req.version, date=req.date, title=req.title, changes=req.changes, reason=req.reason)
db.add(t)
db.commit()
db.refresh(t)
return SeasonalTimelineResponse(id=t.id, version=t.version, date=t.date, title=t.title, changes=t.changes or [], reason=t.reason, sort_order=t.sort_order)
@router.put("/timeline/{t_id}")
def update_timeline(t_id: int, req: SeasonalTimelineSchema, db: Session = Depends(get_db)):
t = db.query(SeasonalProductTimeline).get(t_id)
if not t or t.season != season:
raise HTTPException(status_code=404, detail="时间线不存在")
t.version = req.version
t.date = req.date
t.title = req.title
t.changes = req.changes
t.reason = req.reason
db.commit()
return {"message": "更新成功"}
@router.delete("/timeline/{t_id}")
def delete_timeline(t_id: int, db: Session = Depends(get_db)):
t = db.query(SeasonalProductTimeline).get(t_id)
if not t or t.season != season:
raise HTTPException(status_code=404, detail="时间线不存在")
db.delete(t)
db.commit()
return {"message": "删除成功"}
return router
autumn_router = create_seasonal_router("autumn", "秋季")
winter_router = create_seasonal_router("winter", "冬季")

38
app/routers/selector.py 普通文件
查看文件

@ -0,0 +1,38 @@
from __future__ import annotations
from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from app.database import get_db
from app.auth import get_current_user
from app.models.selector import SelectorConfig
from app.schemas.selector import SelectorConfigSchema, SelectorConfigResponse
router = APIRouter(
prefix="/api/selector",
tags=["选版工具"],
dependencies=[Depends(get_current_user)],
)
def _get_or_create(db: Session) -> SelectorConfig:
config = db.query(SelectorConfig).first()
if not config:
config = SelectorConfig(questions=[], rules=[])
db.add(config)
db.commit()
return config
@router.get("/", response_model=SelectorConfigResponse)
def get_config(db: Session = Depends(get_db)):
return SelectorConfigResponse.model_validate(_get_or_create(db))
@router.put("/", response_model=SelectorConfigResponse)
def update_config(req: SelectorConfigSchema, db: Session = Depends(get_db)):
config = _get_or_create(db)
config.questions = req.questions
config.rules = req.rules
db.commit()
db.refresh(config)
return SelectorConfigResponse.model_validate(config)

32
app/routers/seo.py 普通文件
查看文件

@ -0,0 +1,32 @@
from __future__ import annotations
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from app.database import get_db
from app.auth import get_current_user
from app.models.seo import SeoPage
from app.schemas.seo import SeoPageSchema, SeoPageResponse
router = APIRouter(prefix="/api/seo", tags=["SEO配置"], dependencies=[Depends(get_current_user)])
@router.get("/pages", response_model=list[SeoPageResponse])
def list_pages(db: Session = Depends(get_db)):
pages = db.query(SeoPage).all()
return [SeoPageResponse.model_validate(p) for p in pages]
@router.put("/pages/{page_key}", response_model=SeoPageResponse)
def update_page(page_key: str, req: SeoPageSchema, db: Session = Depends(get_db)):
page = db.query(SeoPage).filter(SeoPage.page_key == page_key).first()
if not page:
raise HTTPException(status_code=404, detail=f"页面 {page_key} 不存在")
page.title = req.title
page.description = req.description
page.h1 = req.h1
page.keywords = req.keywords
page.og_image = req.og_image
page.tldr = req.tldr
db.commit()
db.refresh(page)
return SeoPageResponse.model_validate(page)

49
app/routers/site_images.py 普通文件
查看文件

@ -0,0 +1,49 @@
from __future__ import annotations
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from app.database import get_db
from app.auth import get_current_user
from app.models.site_image import SiteImage
from app.schemas.site_image import SiteImageSchema, SiteImageResponse
router = APIRouter(prefix="/api/site-images", tags=["网站图片"], dependencies=[Depends(get_current_user)])
@router.get("/", response_model=list[SiteImageResponse])
def list_images(db: Session = Depends(get_db)):
images = db.query(SiteImage).order_by(SiteImage.group_name, SiteImage.image_key).all()
return [SiteImageResponse.model_validate(img) for img in images]
@router.put("/{image_id}", response_model=SiteImageResponse)
def update_image(image_id: int, req: SiteImageSchema, db: Session = Depends(get_db)):
img = db.query(SiteImage).get(image_id)
if not img:
raise HTTPException(status_code=404, detail="图片不存在")
img.image_path = req.image_path
img.label = req.label
img.size_hint = req.size_hint
img.description = req.description
db.commit()
db.refresh(img)
return SiteImageResponse.model_validate(img)
@router.post("/", response_model=SiteImageResponse, status_code=201)
def create_image(req: SiteImageSchema, db: Session = Depends(get_db)):
img = SiteImage(**req.model_dump())
db.add(img)
db.commit()
db.refresh(img)
return SiteImageResponse.model_validate(img)
@router.delete("/{image_id}")
def delete_image(image_id: int, db: Session = Depends(get_db)):
img = db.query(SiteImage).get(image_id)
if not img:
raise HTTPException(status_code=404, detail="图片不存在")
db.delete(img)
db.commit()
return {"message": "删除成功"}

69
app/routers/stories.py 普通文件
查看文件

@ -0,0 +1,69 @@
from __future__ import annotations
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session
from sqlalchemy import func as sqlfunc
from app.database import get_db
from app.auth import get_current_user
from app.models.story import Story
from app.schemas.story import StorySchema, StoryResponse, StoryListResponse
router = APIRouter(prefix="/api/stories", tags=["客户故事"], dependencies=[Depends(get_current_user)])
@router.get("/", response_model=StoryListResponse)
def list_stories(
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=100),
keyword: str = Query("", description="搜索关键词"),
db: Session = Depends(get_db),
):
query = db.query(Story)
if keyword:
query = query.filter(Story.title.contains(keyword) | Story.customer_name.contains(keyword))
total = query.count()
items = query.order_by(Story.sort_order, Story.id.desc()).offset((page - 1) * page_size).limit(page_size).all()
return StoryListResponse(
items=[StoryResponse.model_validate(s) for s in items],
total=total,
)
@router.post("/", response_model=StoryResponse, status_code=201)
def create_story(req: StorySchema, db: Session = Depends(get_db)):
max_order = db.query(sqlfunc.max(Story.sort_order)).scalar() or 0
story = Story(**req.model_dump(), sort_order=max_order + 1)
db.add(story)
db.commit()
db.refresh(story)
return StoryResponse.model_validate(story)
@router.put("/{story_id}", response_model=StoryResponse)
def update_story(story_id: int, req: StorySchema, db: Session = Depends(get_db)):
story = db.query(Story).get(story_id)
if not story:
raise HTTPException(status_code=404, detail="故事不存在")
for key, val in req.model_dump().items():
setattr(story, key, val)
db.commit()
db.refresh(story)
return StoryResponse.model_validate(story)
@router.delete("/{story_id}")
def delete_story(story_id: int, db: Session = Depends(get_db)):
story = db.query(Story).get(story_id)
if not story:
raise HTTPException(status_code=404, detail="故事不存在")
db.delete(story)
db.commit()
return {"message": "删除成功"}
@router.put("/reorder/batch")
def reorder_stories(ids: list[int], db: Session = Depends(get_db)):
for i, sid in enumerate(ids):
db.query(Story).filter(Story.id == sid).update({"sort_order": i})
db.commit()
return {"message": "排序更新成功"}

36
app/routers/upload.py 普通文件
查看文件

@ -0,0 +1,36 @@
from __future__ import annotations
import os
import uuid
from fastapi import APIRouter, Depends, UploadFile, File, HTTPException
from app.auth import get_current_user
from app.config import settings
router = APIRouter(prefix="/api/upload", tags=["文件上传"], dependencies=[Depends(get_current_user)])
ALLOWED_EXTENSIONS = {".jpg", ".jpeg", ".png", ".webp", ".gif"}
MAX_SIZE = 5 * 1024 * 1024 # 5MB
@router.post("/image", status_code=201)
async def upload_image(file: UploadFile = File(...), subdir: str = "uploads"):
ext = os.path.splitext(file.filename)[1].lower()
if ext not in ALLOWED_EXTENSIONS:
raise HTTPException(status_code=400, detail=f"不支持的文件格式: {ext}")
content = await file.read()
if len(content) > MAX_SIZE:
raise HTTPException(status_code=400, detail="文件大小不能超过5MB")
target_dir = os.path.join(settings.UPLOAD_DIR, subdir)
os.makedirs(target_dir, exist_ok=True)
filename = f"{uuid.uuid4().hex}{ext}"
filepath = os.path.join(target_dir, filename)
with open(filepath, "wb") as f:
f.write(content)
relative_path = f"/images/{subdir}/{filename}"
return {"url": relative_path, "filename": filename}

172
app/routers/versions.py 普通文件
查看文件

@ -0,0 +1,172 @@
from __future__ import annotations
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from app.database import get_db
from app.auth import get_current_user
from app.models.version import (
VersionConfig, VersionUpgrade, VersionHighlight, VersionCompare,
VersionTimeline, VersionTimelineChange, VersionPhilosophy,
)
from app.schemas.version import (
VersionConfigSchema, VersionUpgradeSchema, VersionHighlightSchema,
VersionCompareSchema, VersionTimelineSchema, VersionTimelineResponse,
VersionPhilosophySchema,
)
router = APIRouter(prefix="/api/versions", tags=["版本历史"], dependencies=[Depends(get_current_user)])
def _get_config(db: Session) -> VersionConfig:
config = db.query(VersionConfig).first()
if not config:
config = VersionConfig()
db.add(config)
db.flush()
return config
@router.get("/config", response_model=VersionConfigSchema)
def get_config(db: Session = Depends(get_db)):
c = _get_config(db)
return VersionConfigSchema(
stats_iterations=c.stats_iterations, stats_years=c.stats_years,
stats_guests=c.stats_guests, quote_text=c.quote_text, quote_author=c.quote_author,
)
@router.put("/config")
def update_config(req: VersionConfigSchema, db: Session = Depends(get_db)):
c = _get_config(db)
for key, val in req.model_dump().items():
setattr(c, key, val)
db.commit()
return {"message": "更新成功"}
@router.get("/upgrades", response_model=list[VersionUpgradeSchema])
def get_upgrades(db: Session = Depends(get_db)):
c = _get_config(db)
items = db.query(VersionUpgrade).filter_by(config_id=c.id).order_by(VersionUpgrade.sort_order).all()
return [VersionUpgradeSchema(tag=u.tag, name=u.name, description=u.description, reason=u.reason) for u in items]
@router.put("/upgrades")
def update_upgrades(req: list[VersionUpgradeSchema], db: Session = Depends(get_db)):
c = _get_config(db)
db.query(VersionUpgrade).filter_by(config_id=c.id).delete()
for i, u in enumerate(req):
db.add(VersionUpgrade(config_id=c.id, tag=u.tag, name=u.name, description=u.description, reason=u.reason, sort_order=i))
db.commit()
return {"message": "更新成功"}
@router.get("/highlights", response_model=list[VersionHighlightSchema])
def get_highlights(db: Session = Depends(get_db)):
c = _get_config(db)
items = db.query(VersionHighlight).filter_by(config_id=c.id).order_by(VersionHighlight.sort_order).all()
return [VersionHighlightSchema(label=h.label, text=h.text) for h in items]
@router.put("/highlights")
def update_highlights(req: list[VersionHighlightSchema], db: Session = Depends(get_db)):
c = _get_config(db)
db.query(VersionHighlight).filter_by(config_id=c.id).delete()
for i, h in enumerate(req):
db.add(VersionHighlight(config_id=c.id, label=h.label, text=h.text, sort_order=i))
db.commit()
return {"message": "更新成功"}
@router.get("/compare", response_model=VersionCompareSchema)
def get_compare(db: Session = Depends(get_db)):
c = _get_config(db)
cmp = db.query(VersionCompare).filter_by(config_id=c.id).first()
if not cmp:
return VersionCompareSchema()
return VersionCompareSchema(headers=cmp.headers or [], rows=cmp.rows or [])
@router.put("/compare")
def update_compare(req: VersionCompareSchema, db: Session = Depends(get_db)):
c = _get_config(db)
cmp = db.query(VersionCompare).filter_by(config_id=c.id).first()
if not cmp:
cmp = VersionCompare(config_id=c.id)
db.add(cmp)
cmp.headers = req.headers
cmp.rows = req.rows
db.commit()
return {"message": "更新成功"}
@router.get("/timeline", response_model=list[VersionTimelineResponse])
def get_timeline(db: Session = Depends(get_db)):
c = _get_config(db)
items = db.query(VersionTimeline).filter_by(config_id=c.id).order_by(VersionTimeline.sort_order).all()
return [
VersionTimelineResponse(
id=t.id, version=t.version, date=t.date, title=t.title, reason=t.reason, sort_order=t.sort_order,
changes=[{"type": ch.type, "text": ch.text} for ch in t.changes],
)
for t in items
]
@router.post("/timeline", response_model=VersionTimelineResponse, status_code=201)
def create_timeline(req: VersionTimelineSchema, db: Session = Depends(get_db)):
c = _get_config(db)
t = VersionTimeline(config_id=c.id, version=req.version, date=req.date, title=req.title, reason=req.reason)
db.add(t)
db.flush()
for i, ch in enumerate(req.changes):
db.add(VersionTimelineChange(timeline_id=t.id, type=ch.type, text=ch.text, sort_order=i))
db.commit()
db.refresh(t)
return VersionTimelineResponse(
id=t.id, version=t.version, date=t.date, title=t.title, reason=t.reason, sort_order=t.sort_order,
changes=[{"type": ch.type, "text": ch.text} for ch in t.changes],
)
@router.put("/timeline/{t_id}")
def update_timeline(t_id: int, req: VersionTimelineSchema, db: Session = Depends(get_db)):
t = db.query(VersionTimeline).get(t_id)
if not t:
raise HTTPException(status_code=404, detail="时间线条目不存在")
t.version = req.version
t.date = req.date
t.title = req.title
t.reason = req.reason
db.query(VersionTimelineChange).filter_by(timeline_id=t.id).delete()
for i, ch in enumerate(req.changes):
db.add(VersionTimelineChange(timeline_id=t.id, type=ch.type, text=ch.text, sort_order=i))
db.commit()
return {"message": "更新成功"}
@router.delete("/timeline/{t_id}")
def delete_timeline(t_id: int, db: Session = Depends(get_db)):
t = db.query(VersionTimeline).get(t_id)
if not t:
raise HTTPException(status_code=404, detail="时间线条目不存在")
db.delete(t)
db.commit()
return {"message": "删除成功"}
@router.get("/philosophy", response_model=list[VersionPhilosophySchema])
def get_philosophy(db: Session = Depends(get_db)):
c = _get_config(db)
items = db.query(VersionPhilosophy).filter_by(config_id=c.id).order_by(VersionPhilosophy.sort_order).all()
return [VersionPhilosophySchema(label=p.label, text=p.text) for p in items]
@router.put("/philosophy")
def update_philosophy(req: list[VersionPhilosophySchema], db: Session = Depends(get_db)):
c = _get_config(db)
db.query(VersionPhilosophy).filter_by(config_id=c.id).delete()
for i, p in enumerate(req):
db.add(VersionPhilosophy(config_id=c.id, label=p.label, text=p.text, sort_order=i))
db.commit()
return {"message": "更新成功"}

133
app/routers/winter_camp.py 普通文件
查看文件

@ -0,0 +1,133 @@
from __future__ import annotations
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from app.database import get_db
from app.auth import get_current_user
from app.models.winter_camp import WinterCampConfig, WinterCampHotel, WinterCampItinerary, WinterCampFaq
from app.schemas.winter_camp import (
WinterCampConfigSchema, WinterCampHotelSchema, WinterCampHotelResponse,
WinterCampItinerarySchema, WinterCampItineraryResponse,
WinterCampFaqSchema, WinterCampFaqResponse,
)
router = APIRouter(prefix="/api/winter-camp", tags=["冬季营"], dependencies=[Depends(get_current_user)])
def _get_config(db: Session) -> WinterCampConfig:
config = db.query(WinterCampConfig).first()
if not config:
config = WinterCampConfig()
db.add(config)
db.flush()
return config
@router.get("/config", response_model=WinterCampConfigSchema)
def get_config(db: Session = Depends(get_db)):
c = _get_config(db)
return WinterCampConfigSchema(
name=c.name, positioning=c.positioning, days=c.days, nights=c.nights,
max_families=c.max_families, total_sessions=c.total_sessions,
age_range=c.age_range, deposit=c.deposit, season=c.season, route=c.route,
why_hulunbuir=c.why_hulunbuir, closing_note=c.closing_note,
photographer=c.photographer, winter_clothing=c.winter_clothing,
camp_advantages=c.camp_advantages or [], service_config=c.service_config or [],
camp_essentials=c.camp_essentials or [],
)
@router.put("/config")
def update_config(req: WinterCampConfigSchema, db: Session = Depends(get_db)):
c = _get_config(db)
c.name = req.name
c.positioning = req.positioning
c.days = req.days
c.nights = req.nights
c.max_families = req.max_families
c.total_sessions = req.total_sessions
c.age_range = req.age_range
c.deposit = req.deposit
c.season = req.season
c.route = req.route
c.why_hulunbuir = req.why_hulunbuir
c.closing_note = req.closing_note
c.photographer = req.photographer
c.winter_clothing = req.winter_clothing
c.camp_advantages = req.camp_advantages
c.service_config = req.service_config
c.camp_essentials = req.camp_essentials
db.commit()
return {"message": "冬季营配置更新成功"}
# --- Hotels ---
@router.get("/hotels", response_model=list[WinterCampHotelResponse])
def list_hotels(db: Session = Depends(get_db)):
rows = db.query(WinterCampHotel).order_by(WinterCampHotel.sort_order).all()
return [WinterCampHotelResponse(id=h.id, name=h.name, star=h.star, nights=h.nights, description=h.description, sort_order=h.sort_order) for h in rows]
@router.put("/hotels")
def update_hotels(items: list[WinterCampHotelSchema], db: Session = Depends(get_db)):
db.query(WinterCampHotel).delete()
for i, h in enumerate(items):
db.add(WinterCampHotel(name=h.name, star=h.star, nights=h.nights, description=h.description, sort_order=i))
db.commit()
return {"message": "酒店信息更新成功"}
# --- Itinerary ---
@router.get("/itinerary", response_model=list[WinterCampItineraryResponse])
def list_itinerary(db: Session = Depends(get_db)):
rows = db.query(WinterCampItinerary).order_by(WinterCampItinerary.sort_order).all()
return [
WinterCampItineraryResponse(id=d.id, day=d.day, title=d.title, summary=d.summary, highlights=d.highlights or [], hotel=d.hotel, sort_order=d.sort_order)
for d in rows
]
@router.put("/itinerary")
def update_itinerary(items: list[WinterCampItinerarySchema], db: Session = Depends(get_db)):
db.query(WinterCampItinerary).delete()
for i, d in enumerate(items):
db.add(WinterCampItinerary(day=d.day, title=d.title, summary=d.summary, highlights=d.highlights, hotel=d.hotel, sort_order=i))
db.commit()
return {"message": "行程更新成功"}
# --- FAQ ---
@router.get("/faq", response_model=list[WinterCampFaqResponse])
def list_faq(db: Session = Depends(get_db)):
rows = db.query(WinterCampFaq).order_by(WinterCampFaq.sort_order).all()
return [WinterCampFaqResponse(id=f.id, question=f.question, answer=f.answer, sort_order=f.sort_order) for f in rows]
@router.post("/faq", response_model=WinterCampFaqResponse, status_code=201)
def create_faq(req: WinterCampFaqSchema, db: Session = Depends(get_db)):
f = WinterCampFaq(question=req.question, answer=req.answer)
db.add(f)
db.commit()
db.refresh(f)
return WinterCampFaqResponse(id=f.id, question=f.question, answer=f.answer, sort_order=f.sort_order)
@router.put("/faq/{f_id}")
def update_faq(f_id: int, req: WinterCampFaqSchema, db: Session = Depends(get_db)):
f = db.query(WinterCampFaq).get(f_id)
if not f:
raise HTTPException(status_code=404, detail="FAQ不存在")
f.question = req.question
f.answer = req.answer
db.commit()
return {"message": "更新成功"}
@router.delete("/faq/{f_id}")
def delete_faq(f_id: int, db: Session = Depends(get_db)):
f = db.query(WinterCampFaq).get(f_id)
if not f:
raise HTTPException(status_code=404, detail="FAQ不存在")
db.delete(f)
db.commit()
return {"message": "删除成功"}

0
app/schemas/__init__.py 普通文件
查看文件

119
app/schemas/about.py 普通文件
查看文件

@ -0,0 +1,119 @@
from __future__ import annotations
from pydantic import BaseModel
from typing import Any
class StorySchema(BaseModel):
title: str | None = None
content: str | None = None
founding_moment: str | None = None
totem_description: str | None = None
totem_tagline: str | None = None
class SubsidiarySchema(BaseModel):
name: str
role: str | None = None
established: str | None = None
class CertificationSchema(BaseModel):
title: str
detail: str | None = None
class TrademarkSchema(BaseModel):
name: str | None = None
scope: str | None = None
holder: str | None = None
description: str | None = None
class CopyrightSchema(BaseModel):
name: str
reg_no: str | None = None
category: str | None = None
holder: str | None = None
date: str | None = None
description: str | None = None
class GuaranteeSchema(BaseModel):
title: str
detail: str | None = None
class XiaohongshuSchema(BaseModel):
account: str | None = None
verified: bool = False
verified_type: str | None = None
followers: str | None = None
likes: str | None = None
awards: list[str] = []
tagline: str | None = None
tags: list[str] = []
description: str | None = None
class TeamSchema(BaseModel):
summary: str | None = None
class CultureValueSchema(BaseModel):
name: str
expression: str | None = None
class CultureSchema(BaseModel):
core: str | None = None
transparency: str | None = None
trust: str | None = None
grief_award_name: str | None = None
grief_award_description: str | None = None
values: list[CultureValueSchema] = []
class FounderDetailSchema(BaseModel):
name: str | None = None
title: str | None = None
brand_founded: int | None = None
years_in_hulunbuir: str | None = None
background: str | None = None
expertise: list[str] = []
media_presence: list[str] = []
class FounderSchema(BaseModel):
name: str
title: str | None = None
story: str | None = None
class MilestoneSchema(BaseModel):
year: int
event: str
class ServicePrincipleSchema(BaseModel):
text: str
class DifferentiationSchema(BaseModel):
name: str
detail: str | None = None
class ServiceMomentSchema(BaseModel):
step: int
name: str
detail: str | None = None
class ServiceJourneySchema(BaseModel):
title: str | None = None
subtitle: str | None = None
moments: list[ServiceMomentSchema] = []
class AboutStatsSchema(BaseModel):
data: dict[str, Any] = {}

29
app/schemas/blog.py 普通文件
查看文件

@ -0,0 +1,29 @@
from __future__ import annotations
from pydantic import BaseModel
from typing import Any
class BlogSchema(BaseModel):
title: str
slug: str
cover_image: str | None = None
summary: str | None = None
content: str | None = None
author: str | None = None
category: str | None = None
tags: list[str] = []
published_at: str | None = None
is_visible: bool = True
class BlogResponse(BlogSchema):
id: int
sort_order: int = 0
class Config:
from_attributes = True
class BlogListResponse(BaseModel):
items: list[BlogResponse]
total: int

44
app/schemas/brand.py 普通文件
查看文件

@ -0,0 +1,44 @@
from __future__ import annotations
from pydantic import BaseModel
from typing import Any
class TrustStatSchema(BaseModel):
value: str
unit: str | None = None
label: str
class DifferentiatorSchema(BaseModel):
title: str
description: str | None = None
class BrandSchema(BaseModel):
name: str
full_name: str | None = None
domain: str | None = None
url: str | None = None
slogan_emotional: str | None = None
slogan_functional: str | None = None
icp_entity: str | None = None
icp: str | None = None
icp_url: str | None = None
e_contract: str | None = None
cta_buttons: dict[str, Any] | None = None
conversion_path: list[Any] | None = None
class BrandFullSchema(BrandSchema):
id: int
trust_stats: list[TrustStatSchema] = []
differentiators: list[DifferentiatorSchema] = []
class Config:
from_attributes = True
class BrandUpdateRequest(BaseModel):
brand: BrandSchema
trust_stats: list[TrustStatSchema] = []
differentiators: list[DifferentiatorSchema] = []

26
app/schemas/calendar.py 普通文件
查看文件

@ -0,0 +1,26 @@
from __future__ import annotations
from pydantic import BaseModel
class CalendarEventSchema(BaseModel):
date: str | None = None
title: str
description: str | None = None
class CalendarMonthSchema(BaseModel):
year: int
month: int
title: str | None = None
description: str | None = None
weather: str | None = None
highlights: list[str] = []
events: list[CalendarEventSchema] = []
is_available: bool = True
class CalendarMonthResponse(CalendarMonthSchema):
id: int
class Config:
from_attributes = True

23
app/schemas/contact.py 普通文件
查看文件

@ -0,0 +1,23 @@
from __future__ import annotations
from pydantic import BaseModel
class ContactChannelSchema(BaseModel):
type: str
label: str
value: str | None = None
qr_image: str | None = None
is_primary: bool = False
description: str | None = None
class ContactChannelResponse(ContactChannelSchema):
id: int
sort_order: int = 0
class Config:
from_attributes = True
class ContactConfigSchema(BaseModel):
security_notice: str | None = None

15
app/schemas/courses.py 普通文件
查看文件

@ -0,0 +1,15 @@
from __future__ import annotations
from pydantic import BaseModel
class CoursesConfigSchema(BaseModel):
modules: list = []
age_groups: list = []
faqs: list = []
class CoursesConfigResponse(CoursesConfigSchema):
id: int
class Config:
from_attributes = True

查看文件

@ -0,0 +1,12 @@
from __future__ import annotations
from pydantic import BaseModel
from typing import Any
class CustomizeConfigSchema(BaseModel):
trust_stats: list[Any] = []
durations: list[Any] = []
activities: list[Any] = []
budgets: list[Any] = []
process: list[Any] = []
contact: dict[str, Any] = {}

查看文件

@ -0,0 +1,38 @@
from __future__ import annotations
from pydantic import BaseModel
from typing import Literal
class CustomizeSubmissionResponse(BaseModel):
id: int
name: str | None = None
phone: str | None = None
wechat: str | None = None
adults: int = 0
children: int = 0
travel_dates: str | None = None
budget: str | None = None
interests: list = []
notes: str | None = None
source: str | None = None
status: str = "pending"
created_at: str | None = None
class Config:
from_attributes = True
@classmethod
def model_validate(cls, obj, **kwargs):
data = super().model_validate(obj, **kwargs)
if obj.created_at:
data.created_at = obj.created_at.strftime("%Y-%m-%d %H:%M:%S")
return data
class CustomizeSubmissionListResponse(BaseModel):
items: list[CustomizeSubmissionResponse]
total: int
class CustomizeStatusUpdate(BaseModel):
status: Literal["pending", "processed"]

54
app/schemas/destination.py 普通文件
查看文件

@ -0,0 +1,54 @@
from __future__ import annotations
from pydantic import BaseModel
class DestinationConfigSchema(BaseModel):
title: str | None = None
subtitle: str | None = None
intro: str | None = None
closing_title: str | None = None
closing_text: str | None = None
data_sources: str | None = None
honest_title: str | None = None
honest_subtitle: str | None = None
class DestinationItemSchema(BaseModel):
dest_id: str
name: str
tag: str | None = None
highlight: bool = False
class DestinationItemResponse(DestinationItemSchema):
id: int
sort_order: int = 0
class Config:
from_attributes = True
class DestinationDimensionSchema(BaseModel):
label: str
icon: str | None = None
values: list[str] = []
class DestinationDimensionResponse(DestinationDimensionSchema):
id: int
sort_order: int = 0
class Config:
from_attributes = True
class DestinationHonestItemSchema(BaseModel):
text: str
class DestinationHonestItemResponse(DestinationHonestItemSchema):
id: int
sort_order: int = 0
class Config:
from_attributes = True

查看文件

@ -0,0 +1,35 @@
from __future__ import annotations
from pydantic import BaseModel, field_validator
class DestinationDetailSchema(BaseModel):
name: str
slug: str
subtitle: str | None = None
cover_image: str | None = None
description: str | None = None
location: str | None = None
best_season: str | None = None
duration: str | None = None
highlights: list = []
gallery: list = []
tags: list = []
is_visible: bool = True
@field_validator('highlights', 'gallery', 'tags', mode='before')
@classmethod
def coerce_none_to_list(cls, v):
return v if v is not None else []
class DestinationDetailResponse(DestinationDetailSchema):
id: int
sort_order: int = 0
class Config:
from_attributes = True
class DestinationDetailListResponse(BaseModel):
items: list[DestinationDetailResponse]
total: int

19
app/schemas/export.py 普通文件
查看文件

@ -0,0 +1,19 @@
from __future__ import annotations
from pydantic import BaseModel
class ExportRequest(BaseModel):
modules: list[str] = ["all"]
trigger_rebuild: bool = False
class ExportLogResponse(BaseModel):
id: int
exported_by: str | None
modules: list[str] | None
status: str
message: str | None
created_at: str | None
class Config:
from_attributes = True

38
app/schemas/faq.py 普通文件
查看文件

@ -0,0 +1,38 @@
from __future__ import annotations
from pydantic import BaseModel
from typing import Any
class RelatedLink(BaseModel):
text: str
url: str
class FaqQuestionSchema(BaseModel):
question_id: str
question: str
answer: str
related_links: list[RelatedLink] = []
class FaqQuestionResponse(FaqQuestionSchema):
id: int
category_id: int
sort_order: int = 0
class Config:
from_attributes = True
class FaqCategorySchema(BaseModel):
category_id: str
name: str
class FaqCategoryResponse(FaqCategorySchema):
id: int
sort_order: int = 0
questions: list[FaqQuestionResponse] = []
class Config:
from_attributes = True

25
app/schemas/gallery.py 普通文件
查看文件

@ -0,0 +1,25 @@
from __future__ import annotations
from pydantic import BaseModel
class GalleryItemSchema(BaseModel):
title: str | None = None
image: str
photographer: str | None = None
location: str | None = None
description: str | None = None
tags: list = []
is_visible: bool = True
class GalleryItemResponse(GalleryItemSchema):
id: int
sort_order: int = 0
class Config:
from_attributes = True
class GalleryListResponse(BaseModel):
items: list[GalleryItemResponse]
total: int

24
app/schemas/guide.py 普通文件
查看文件

@ -0,0 +1,24 @@
from __future__ import annotations
from pydantic import BaseModel
from typing import Any
class GuideConfigSchema(BaseModel):
page_intro: str | None = None
class GuideSectionSchema(BaseModel):
section_id: str
title: str | None = None
subtitle: str | None = None
icon: str | None = None
content: str | None = None
data: Any = None
class GuideSectionResponse(GuideSectionSchema):
id: int
sort_order: int = 0
class Config:
from_attributes = True

22
app/schemas/navigation.py 普通文件
查看文件

@ -0,0 +1,22 @@
from __future__ import annotations
from pydantic import BaseModel
class NavLinkSchema(BaseModel):
text: str
to: str
class NavFooterLinkSchema(BaseModel):
text: str
to: str
class NavFooterGroupSchema(BaseModel):
title: str
links: list[NavFooterLinkSchema] = []
class NavigationSchema(BaseModel):
header: list[NavLinkSchema] = []
footer: list[NavFooterGroupSchema] = []

26
app/schemas/news.py 普通文件
查看文件

@ -0,0 +1,26 @@
from __future__ import annotations
from pydantic import BaseModel
class NewsSchema(BaseModel):
title: str
cover_image: str | None = None
summary: str | None = None
content: str | None = None
source: str | None = None
source_url: str | None = None
published_at: str | None = None
is_visible: bool = True
class NewsResponse(NewsSchema):
id: int
sort_order: int = 0
class Config:
from_attributes = True
class NewsListResponse(BaseModel):
items: list[NewsResponse]
total: int

18
app/schemas/partner.py 普通文件
查看文件

@ -0,0 +1,18 @@
from __future__ import annotations
from pydantic import BaseModel
class PartnerSchema(BaseModel):
name: str
logo: str | None = None
website: str | None = None
description: str | None = None
category: str | None = None
class PartnerResponse(PartnerSchema):
id: int
sort_order: int = 0
class Config:
from_attributes = True

27
app/schemas/pricing.py 普通文件
查看文件

@ -0,0 +1,27 @@
from __future__ import annotations
from pydantic import BaseModel, field_validator
class PricingItemSchema(BaseModel):
product_name: str
product_slug: str | None = None
price_from: str | None = None
price_unit: str | None = None
price_label: str | None = None
description: str | None = None
features: list[str] = []
notes: list[str] = []
is_visible: bool = True
@field_validator('features', 'notes', mode='before')
@classmethod
def coerce_none_to_list(cls, v):
return v if v is not None else []
class PricingItemResponse(PricingItemSchema):
id: int
sort_order: int = 0
class Config:
from_attributes = True

51
app/schemas/product.py 普通文件
查看文件

@ -0,0 +1,51 @@
from __future__ import annotations
from pydantic import BaseModel
from typing import Any
class ProductVersionSchema(BaseModel):
version_id: str
name: str
days: int
nights: int
audience: str | None = None
description: str | None = None
highlights: list[str] = []
tag: str | None = None
class ProductVersionResponse(ProductVersionSchema):
id: int
sort_order: int = 0
class Config:
from_attributes = True
class SummerCampFaqSchema(BaseModel):
question: str
answer: str
class SummerCampSchema(BaseModel):
name: str | None = None
positioning: str | None = None
days: int | None = None
nights: int | None = None
sessions: dict | None = None
difference_from_v9: str | None = None
principles: list[str] = []
activities: list[str] = []
itinerary: list[str] = []
faq: list[SummerCampFaqSchema] = []
class SelectionGuideSchema(BaseModel):
by_vacation_length: list[Any] = []
by_child_age: list[Any] = []
by_preference: list[Any] = []
class ProductConfigSchema(BaseModel):
narrative: str | None = None
pricing_philosophy: str | None = None

查看文件

@ -0,0 +1,19 @@
from __future__ import annotations
from pydantic import BaseModel
class QualificationSchema(BaseModel):
title: str
issuer: str | None = None
year: str | None = None
image: str | None = None
description: str | None = None
category: str | None = None
class QualificationResponse(QualificationSchema):
id: int
sort_order: int = 0
class Config:
from_attributes = True

32
app/schemas/review.py 普通文件
查看文件

@ -0,0 +1,32 @@
from __future__ import annotations
from pydantic import BaseModel
class ReviewSummarySchema(BaseModel):
total_count: int = 0
approval_rate: str | None = None
keywords: list[str] = []
class ReviewSchema(BaseModel):
nickname: str
travel_date: str | None = None
product_version: str | None = None
screenshot: str | None = None
content: str
scenes: list[str] = []
concerns: list[str] = []
is_visible: bool = True
class ReviewResponse(ReviewSchema):
id: int
sort_order: int = 0
class Config:
from_attributes = True
class ReviewListResponse(BaseModel):
items: list[ReviewResponse]
total: int

查看文件

@ -0,0 +1,63 @@
from __future__ import annotations
from pydantic import BaseModel
from typing import Any
class SeasonalConfigSchema(BaseModel):
narrative: str | None = None
style: str | None = None
brand_name: str | None = None
version_label: str | None = None
season_label: str | None = None
class SeasonalVersionSchema(BaseModel):
version_id: str
name: str
days: int
nights: int
tag: str | None = None
line: str | None = None
route: str | None = None
audience: str | None = None
description: str | None = None
highlights: list[str] = []
itinerary: list[str] = []
class SeasonalVersionResponse(SeasonalVersionSchema):
id: int
sort_order: int = 0
class Config:
from_attributes = True
class SeasonalHighlightSchema(BaseModel):
category: str # shared, south, north
title: str
description: str | None = None
class SeasonalHighlightResponse(SeasonalHighlightSchema):
id: int
sort_order: int = 0
class Config:
from_attributes = True
class SeasonalTimelineSchema(BaseModel):
version: str | None = None
date: str | None = None
title: str | None = None
changes: list[dict] = []
reason: str | None = None
class SeasonalTimelineResponse(SeasonalTimelineSchema):
id: int
sort_order: int = 0
class Config:
from_attributes = True

23
app/schemas/selector.py 普通文件
查看文件

@ -0,0 +1,23 @@
from __future__ import annotations
from pydantic import BaseModel, field_validator
class SelectorConfigSchema(BaseModel):
questions: list = []
rules: list = []
@field_validator('questions', 'rules', mode='before')
@classmethod
def coerce_to_list(cls, v):
if v is None:
return []
if isinstance(v, list):
return v
return [] # dict等旧格式数据降级为空列表
class SelectorConfigResponse(SelectorConfigSchema):
id: int
class Config:
from_attributes = True

19
app/schemas/seo.py 普通文件
查看文件

@ -0,0 +1,19 @@
from __future__ import annotations
from pydantic import BaseModel
class SeoPageSchema(BaseModel):
title: str | None = None
description: str | None = None
h1: str | None = None
keywords: str | None = None
og_image: str | None = None
tldr: str | None = None
class SeoPageResponse(SeoPageSchema):
id: int
page_key: str
class Config:
from_attributes = True

18
app/schemas/site_image.py 普通文件
查看文件

@ -0,0 +1,18 @@
from __future__ import annotations
from pydantic import BaseModel
class SiteImageSchema(BaseModel):
group_name: str
image_key: str
image_path: str
label: str | None = None
size_hint: str | None = None
description: str | None = None
class SiteImageResponse(SiteImageSchema):
id: int
class Config:
from_attributes = True

28
app/schemas/story.py 普通文件
查看文件

@ -0,0 +1,28 @@
from __future__ import annotations
from pydantic import BaseModel
class StorySchema(BaseModel):
title: str
cover_image: str | None = None
summary: str | None = None
content: str | None = None
customer_name: str | None = None
avatar: str | None = None
travel_date: str | None = None
product_name: str | None = None
scenes: list[str] = []
is_visible: bool = True
class StoryResponse(StorySchema):
id: int
sort_order: int = 0
class Config:
from_attributes = True
class StoryListResponse(BaseModel):
items: list[StoryResponse]
total: int

55
app/schemas/user.py 普通文件
查看文件

@ -0,0 +1,55 @@
from __future__ import annotations
from pydantic import BaseModel
class LoginRequest(BaseModel):
username: str
password: str
class LoginResponse(BaseModel):
access_token: str
token_type: str = "bearer"
class UserInfo(BaseModel):
id: int
username: str
display_name: str | None
role: str
class Config:
from_attributes = True
class ChangePasswordRequest(BaseModel):
old_password: str
new_password: str
class CreateUserRequest(BaseModel):
username: str
password: str
display_name: str | None = None
role: str = "editor"
class UpdateUserRequest(BaseModel):
display_name: str | None = None
role: str | None = None
is_active: bool | None = None
class ResetPasswordRequest(BaseModel):
new_password: str
class UserListResponse(BaseModel):
id: int
username: str
display_name: str | None
role: str
is_active: bool
class Config:
from_attributes = True

54
app/schemas/version.py 普通文件
查看文件

@ -0,0 +1,54 @@
from __future__ import annotations
from pydantic import BaseModel
from typing import Any
class VersionUpgradeSchema(BaseModel):
tag: str | None = None
name: str | None = None
description: str | None = None
reason: str | None = None
class VersionHighlightSchema(BaseModel):
label: str | None = None
text: str | None = None
class VersionCompareSchema(BaseModel):
headers: list[str] = []
rows: list[list[str]] = []
class TimelineChangeSchema(BaseModel):
type: str # add, update, remove
text: str
class VersionTimelineSchema(BaseModel):
version: str
date: str | None = None
title: str | None = None
changes: list[TimelineChangeSchema] = []
reason: str | None = None
class VersionTimelineResponse(VersionTimelineSchema):
id: int
sort_order: int = 0
class Config:
from_attributes = True
class VersionPhilosophySchema(BaseModel):
label: str | None = None
text: str | None = None
class VersionConfigSchema(BaseModel):
stats_iterations: int | None = None
stats_years: int | None = None
stats_guests: str | None = None
quote_text: str | None = None
quote_author: str | None = None

67
app/schemas/winter_camp.py 普通文件
查看文件

@ -0,0 +1,67 @@
from __future__ import annotations
from pydantic import BaseModel
from typing import Any
class WinterCampConfigSchema(BaseModel):
name: str | None = None
positioning: str | None = None
days: int | None = None
nights: int | None = None
max_families: int | None = None
total_sessions: int | None = None
age_range: str | None = None
deposit: int | None = None
season: str | None = None
route: str | None = None
why_hulunbuir: str | None = None
closing_note: str | None = None
photographer: dict | None = None
winter_clothing: dict | None = None
camp_advantages: list[str] = []
service_config: list[str] = []
camp_essentials: list[str] = []
class WinterCampHotelSchema(BaseModel):
name: str
star: str | None = None
nights: str | None = None
description: str | None = None
class WinterCampHotelResponse(WinterCampHotelSchema):
id: int
sort_order: int = 0
class Config:
from_attributes = True
class WinterCampItinerarySchema(BaseModel):
day: int
title: str | None = None
summary: str | None = None
highlights: list[str] = []
hotel: str | None = None
class WinterCampItineraryResponse(WinterCampItinerarySchema):
id: int
sort_order: int = 0
class Config:
from_attributes = True
class WinterCampFaqSchema(BaseModel):
question: str
answer: str
class WinterCampFaqResponse(WinterCampFaqSchema):
id: int
sort_order: int = 0
class Config:
from_attributes = True

某些文件未显示,因为此 diff 中更改的文件太多 显示更多