Initial commit: hulai admin API backend
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
这个提交包含在:
当前提交
8c0a5f489d
11
.claude/launch.json
普通文件
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
普通文件
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
普通文件
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
普通文件
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
普通文件
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
普通文件
0
app/__init__.py
普通文件
51
app/auth.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
普通文件
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
普通文件
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
普通文件
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
普通文件
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
普通文件
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
普通文件
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
普通文件
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
普通文件
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
普通文件
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
普通文件
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())
|
||||||
19
app/models/customize_config.py
普通文件
19
app/models/customize_config.py
普通文件
@ -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
普通文件
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)
|
||||||
26
app/models/destination_detail.py
普通文件
26
app/models/destination_detail.py
普通文件
@ -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
普通文件
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
普通文件
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
普通文件
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
普通文件
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
普通文件
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
普通文件
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
普通文件
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
普通文件
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
普通文件
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
普通文件
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
普通文件
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())
|
||||||
66
app/models/seasonal_product.py
普通文件
66
app/models/seasonal_product.py
普通文件
@ -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
普通文件
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
普通文件
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
普通文件
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
普通文件
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
普通文件
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
普通文件
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
普通文件
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
普通文件
0
app/routers/__init__.py
普通文件
361
app/routers/about.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
普通文件
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
普通文件
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
普通文件
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
普通文件
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
普通文件
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
普通文件
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
普通文件
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
普通文件
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
普通文件
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
普通文件
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
普通文件
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
普通文件
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
普通文件
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
普通文件
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
普通文件
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
普通文件
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
普通文件
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": "选品指南更新成功"}
|
||||||
57
app/routers/qualifications.py
普通文件
57
app/routers/qualifications.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.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
普通文件
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": "排序更新成功"}
|
||||||
174
app/routers/seasonal_products.py
普通文件
174
app/routers/seasonal_products.py
普通文件
@ -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
普通文件
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
普通文件
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
普通文件
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
普通文件
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
普通文件
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
普通文件
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
普通文件
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
普通文件
0
app/schemas/__init__.py
普通文件
119
app/schemas/about.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
普通文件
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
普通文件
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
普通文件
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
普通文件
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
普通文件
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
|
||||||
12
app/schemas/customize_config.py
普通文件
12
app/schemas/customize_config.py
普通文件
@ -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
普通文件
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
普通文件
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
普通文件
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
普通文件
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
普通文件
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
普通文件
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
普通文件
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
普通文件
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
普通文件
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
普通文件
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
|
||||||
19
app/schemas/qualification.py
普通文件
19
app/schemas/qualification.py
普通文件
@ -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
普通文件
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
|
||||||
63
app/schemas/seasonal_product.py
普通文件
63
app/schemas/seasonal_product.py
普通文件
@ -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
普通文件
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
普通文件
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
普通文件
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
普通文件
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
普通文件
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
普通文件
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
普通文件
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 中更改的文件太多 显示更多
正在加载...
x
在新工单中引用
屏蔽一个用户