Initial commit: hulai admin API backend

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
这个提交包含在:
刘涛
2026-03-22 22:01:23 +08:00
共同撰写人 Claude Sonnet 4.6
当前提交 8c0a5f489d
共修改 114 个文件,包含 7619 行新增和 0 行删除
查看文件
+361
查看文件
@@ -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
查看文件
@@ -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
查看文件
@@ -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
查看文件
@@ -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
查看文件
@@ -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
查看文件
@@ -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
查看文件
@@ -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
查看文件
@@ -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
查看文件
@@ -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": "坦诚说明更新成功"}
+92
查看文件
@@ -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
查看文件
@@ -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
查看文件
@@ -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
查看文件
@@ -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
查看文件
@@ -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
查看文件
@@ -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
查看文件
@@ -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
查看文件
@@ -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
查看文件
@@ -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
查看文件
@@ -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
查看文件
@@ -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
查看文件
@@ -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
查看文件
@@ -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
查看文件
@@ -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
查看文件
@@ -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
查看文件
@@ -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
查看文件
@@ -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
查看文件
@@ -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
查看文件
@@ -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
查看文件
@@ -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": "删除成功"}