refactor: 清理废弃模块并重构管理端与全端代码

- 移除所有废弃的旧首页配置模块,包括destinationHero、demand相关、HomeExperience、vehicleService等表结构与代码,新增数据库迁移删除遗留表
- 重构管理端后台布局为标准RuoYi风格,新增Sidebar、Navbar、SettingsDrawer等组件,替换旧的过渡菜单实现
- 调整移动端响应断点为768px,更新布局相关测试用例,新增布局组件测试
- 删除OperationsTools发布/重置页面,移除/tools菜单入口,清理对应的API调用与测试代码
- 重构玩法模块编辑器:替换旧的图片手动输入框为图片上传组件,移除冗余的取消按钮
- 新增线索查询offset参数支持,完善查询参数处理逻辑
- 优化rbac菜单构建逻辑,新增可见性控制参数
- 修正管家编辑器提示文案,优化系统资源编辑器表单初始化逻辑
- 清理小程序端废弃的体验推荐组件与相关测试代码
- 更新文档与种子脚本,匹配新的API契约与使用说明
This commit is contained in:
duanshuwen committed 2026-08-26 23:24:28 +08:00
1 parent f6c5ad589f
commit fbf2c772a8
63 files changed
+2272 -1991

No files matched your search

-61
View File
@@ -19,14 +19,6 @@ HERO_SLIDES = [
},
]
DESTINATION_HERO = [
{
"title": "山水、苗寨、古城与野咖同程安排",
"kicker": "贵州小包团目的地",
"image": "/assets/guizhou/huangguoshu-waterfall.jpg",
},
]
VEHICLE_OPTIONS = [
{
"title": "5座舒适用车",
@@ -39,56 +31,3 @@ VEHICLE_OPTIONS = [
"description": "适合5-8人同行、亲子或长辈出行,留足行李和休息空间。",
},
]
VEHICLE_SERVICE = {
"introTitle": "旅游出行专属用车服务",
"intro": "覆盖景区接送、短途包车、旅途代步和多人出行场景。提交需求后由管家联系确认车型与服务安排。",
"serviceSections": [
{
"title": "旅游专属包车",
"description": "适配家庭出游、朋友结伴和小团体旅行,支持半天或全天包车,行程可按需求沟通调整。",
},
{
"title": "多车型灵活选择",
"description": "舒适轿车、SUV、商务车等车型按人数和行李情况匹配,让不同规模的旅行都能舒适出行。",
},
],
"advantages": [
"专为旅游定制:贴合出游场景,行程灵活",
"车型齐全:按人数和行李匹配出行方案",
"省心便捷:线上提交需求,管家快速跟进",
"出行安心:服务安排清晰,行程有人跟进",
],
"processSteps": [
{"title": "提交需求", "description": "填写日期、地点、人数和车型偏好。"},
{"title": "管家联系", "description": "管家根据需求沟通车辆和服务安排。"},
{"title": "确认服务", "description": "确认行程细节与报价后按约定出行。"},
{"title": "安心出行", "description": "按确认方案完成接送或包车服务。"},
],
}
DEMAND_HERO = [
{
"kicker": "3步定制",
"title": "告诉我们日期、人数和想法",
"description": "管家会按同行人、预算和体力强度,重新组合酒店、用车和景点节奏。",
"steps": ["提交需求", "管家沟通", "确认方案"],
}
]
DEMAND_FEATURE_CARDS = [
{"title": "动线", "description": "按天数顺路排"},
{"title": "住宿", "description": "城市与山野组合"},
{"title": "用车", "description": "人数行李匹配"},
]
DEMAND_FORM = {
"destinationLabel": "目的地/玩法",
"destinationPlaceholder": "例如:贵州、黄果树、西江苗寨",
"phoneLabel": "联系方式",
"phonePlaceholder": "手机号 / 微信号",
"noteLabel": "补充说明",
"notePlaceholder": "出行日期、人数、酒店偏好、预算范围",
"submitLabel": "提交出行需求",
"chips": ["贵州", "黄果树", "荔波小七孔", "西江苗寨", "梵净山", "万峰林"],
}
+1 -98
View File
@@ -1,7 +1,7 @@
from datetime import datetime, timezone
from uuid import uuid4
from sqlalchemy import Boolean, DateTime, ForeignKey, Integer, String, Text, UniqueConstraint
from sqlalchemy.dialects.postgresql import ARRAY, JSONB
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import Mapped, mapped_column, relationship
from .database import Base
@@ -155,19 +155,6 @@ class HeroSlide(AdminOwnedContent, Base):
updatedAt: Mapped[datetime] = mapped_column(DateTime, default=utc_now, onupdate=utc_now, nullable=False)
class DestinationHero(AdminOwnedContent, Base):
__tablename__ = "DestinationHero"
id: Mapped[str] = mapped_column(String, primary_key=True, default=new_id)
title: Mapped[str] = mapped_column(String, nullable=False)
kicker: Mapped[str | None] = mapped_column(String)
image: Mapped[str | None] = mapped_column(String)
sortOrder: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
isActive: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
createdAt: Mapped[datetime] = mapped_column(DateTime, default=utc_now, nullable=False)
updatedAt: Mapped[datetime] = mapped_column(DateTime, default=utc_now, onupdate=utc_now, nullable=False)
class VehicleOption(AdminOwnedContent, Base):
__tablename__ = "VehicleOption"
@@ -181,36 +168,6 @@ class VehicleOption(AdminOwnedContent, Base):
updatedAt: Mapped[datetime] = mapped_column(DateTime, default=utc_now, onupdate=utc_now, nullable=False)
class VehicleServiceConfig(AdminOwnedContent, Base):
__tablename__ = "VehicleServiceConfig"
id: Mapped[str] = mapped_column(String, primary_key=True, default=new_id)
introTitle: Mapped[str] = mapped_column(String, nullable=False)
intro: Mapped[str] = mapped_column(Text, nullable=False)
serviceSections: Mapped[list[dict[str, str]]] = mapped_column(JSONB, default=list, nullable=False)
advantages: Mapped[list[str]] = mapped_column(JSONB, default=list, nullable=False)
processSteps: Mapped[list[dict[str, str]]] = mapped_column(JSONB, default=list, nullable=False)
isActive: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
createdAt: Mapped[datetime] = mapped_column(DateTime, default=utc_now, nullable=False)
updatedAt: Mapped[datetime] = mapped_column(DateTime, default=utc_now, onupdate=utc_now, nullable=False)
class HomeExperience(AdminOwnedContent, Base):
__tablename__ = "HomeExperience"
id: Mapped[str] = mapped_column(String, primary_key=True, default=new_id)
badge: Mapped[str] = mapped_column(String, nullable=False)
category: Mapped[str] = mapped_column(String, nullable=False)
title: Mapped[str] = mapped_column(String, nullable=False)
englishTitle: Mapped[str] = mapped_column(String, nullable=False)
image: Mapped[str] = mapped_column(String, nullable=False)
demandKeyword: Mapped[str] = mapped_column(String, nullable=False)
isActive: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False, index=True)
sortOrder: Mapped[int] = mapped_column(Integer, default=0, nullable=False, index=True)
createdAt: Mapped[datetime] = mapped_column(DateTime, default=utc_now, nullable=False)
updatedAt: Mapped[datetime] = mapped_column(DateTime, default=utc_now, onupdate=utc_now, nullable=False)
class HomeTeamBuilding(AdminOwnedContent, Base):
__tablename__ = "HomeTeamBuilding"
@@ -338,49 +295,6 @@ class ConciergeAdvisor(AdminOwnedContent, Base):
updatedAt: Mapped[datetime] = mapped_column(DateTime, default=utc_now, onupdate=utc_now, nullable=False)
class DemandHero(AdminOwnedContent, Base):
__tablename__ = "DemandHero"
id: Mapped[str] = mapped_column(String, primary_key=True, default=new_id)
title: Mapped[str] = mapped_column(String, nullable=False)
kicker: Mapped[str | None] = mapped_column(String)
description: Mapped[str | None] = mapped_column(Text)
steps: Mapped[list[str]] = mapped_column(ARRAY(String), default=list, nullable=False)
sortOrder: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
isActive: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
createdAt: Mapped[datetime] = mapped_column(DateTime, default=utc_now, nullable=False)
updatedAt: Mapped[datetime] = mapped_column(DateTime, default=utc_now, onupdate=utc_now, nullable=False)
class DemandFeatureCard(AdminOwnedContent, Base):
__tablename__ = "DemandFeatureCard"
id: Mapped[str] = mapped_column(String, primary_key=True, default=new_id)
title: Mapped[str] = mapped_column(String, nullable=False)
description: Mapped[str | None] = mapped_column(Text)
sortOrder: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
isActive: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
createdAt: Mapped[datetime] = mapped_column(DateTime, default=utc_now, nullable=False)
updatedAt: Mapped[datetime] = mapped_column(DateTime, default=utc_now, onupdate=utc_now, nullable=False)
class DemandForm(AdminOwnedContent, Base):
__tablename__ = "DemandForm"
id: Mapped[str] = mapped_column(String, primary_key=True, default=new_id)
destinationLabel: Mapped[str] = mapped_column(String, default="目的地/玩法", nullable=False)
destinationPlaceholder: Mapped[str | None] = mapped_column(String)
phoneLabel: Mapped[str] = mapped_column(String, default="联系方式", nullable=False)
phonePlaceholder: Mapped[str | None] = mapped_column(String)
noteLabel: Mapped[str] = mapped_column(String, default="补充说明", nullable=False)
notePlaceholder: Mapped[str | None] = mapped_column(String)
submitLabel: Mapped[str] = mapped_column(String, default="提交出行需求", nullable=False)
chips: Mapped[list[str]] = mapped_column(ARRAY(String), default=list, nullable=False)
isActive: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
createdAt: Mapped[datetime] = mapped_column(DateTime, default=utc_now, nullable=False)
updatedAt: Mapped[datetime] = mapped_column(DateTime, default=utc_now, onupdate=utc_now, nullable=False)
class Lead(AdminOwnedContent, Base):
__tablename__ = "Lead"
@@ -447,17 +361,6 @@ class Order(Base):
customer: Mapped[Customer | None] = relationship(back_populates="orders")
class SiteVersion(Base):
__tablename__ = "SiteVersion"
id: Mapped[str] = mapped_column(String, primary_key=True, default=new_id)
title: Mapped[str] = mapped_column(String, nullable=False)
status: Mapped[str] = mapped_column(String, default="draft", nullable=False)
snapshot: Mapped[dict | list] = mapped_column(JSONB, nullable=False)
publishedAt: Mapped[datetime | None] = mapped_column(DateTime)
createdAt: Mapped[datetime] = mapped_column(DateTime, default=utc_now, nullable=False)
class AuditLog(Base):
__tablename__ = "AuditLog"
+6 -2
View File
@@ -13,8 +13,12 @@ from .redis_session import AdminSessionStore, RedisUnavailableError
DATA_SCOPE_VALUES = {"all", "dept", "dept_and_children", "custom_dept", "self"}
def build_menu_tree(menus: Iterable[AdminMenu]) -> list[dict[str, Any]]:
active_menus = [menu for menu in menus if menu.isActive and (menu.type == "button" or menu.isVisible)]
def build_menu_tree(menus: Iterable[AdminMenu], visible_only: bool = True) -> list[dict[str, Any]]:
active_menus = [
menu
for menu in menus
if not visible_only or (menu.isActive and (menu.type == "button" or menu.isVisible))
]
nodes: dict[str, dict[str, Any]] = {
menu.id: {
"id": menu.id,
+21 -169
View File
@@ -19,20 +19,13 @@ from ..models import (
AuditLog,
ConciergeAdvisor,
DetailRecord,
DemandFeatureCard,
DemandForm,
DemandHero,
DestinationHero,
HeroSlide,
HomeExperience,
HomeTeamBuilding,
HomeWanfaRecommendation,
HomeWildArchive,
Lead,
MediaAsset,
SiteVersion,
VehicleOption,
VehicleServiceConfig,
WanfaCategory,
WanfaRoute,
utc_now,
@@ -44,8 +37,6 @@ from ..schemas import (
LeadStatus,
LeadStatusIn,
LoginIn,
HomeExperienceCreate,
HomeExperiencePatch,
HomeReorderIn,
HomeTeamBuildingCreate,
HomeTeamBuildingPatch,
@@ -65,7 +56,7 @@ from ..schemas import (
DetailCreate,
DetailPatch,
)
from ..seed import create_media, reset_guizhou_content
from ..seed import create_media
from ..serializers import concierge_advisor_dict, detail_record_dict, encode_value, lead_dict, model_dict
from .shared import site_config
@@ -90,63 +81,6 @@ SITE_CONFIG_MODULES = {
"none_to_empty": {"image"},
"create_defaults": {"kicker": "", "image": ""},
},
"destinationHero": {
"model": DestinationHero,
"entity": "destination_hero",
"primary": "title",
"fields": {"title", "kicker", "image", "isActive", "sortOrder"},
"none_to_empty": set(),
"empty_to_none": {"kicker", "image"},
"create_defaults": {"kicker": None, "image": None},
},
"demandHero": {
"model": DemandHero,
"entity": "demand_hero",
"primary": "title",
"fields": {"title", "kicker", "description", "steps", "isActive", "sortOrder"},
"none_to_empty": set(),
"empty_to_none": {"kicker", "description"},
"create_defaults": {"kicker": None, "description": None, "steps": []},
},
"demandFeatureCards": {
"model": DemandFeatureCard,
"entity": "demand_feature_card",
"primary": "title",
"fields": {"title", "description", "isActive", "sortOrder"},
"none_to_empty": set(),
"empty_to_none": {"description"},
"create_defaults": {"description": None},
},
"demandForm": {
"model": DemandForm,
"entity": "demand_form",
"primary": "submitLabel",
"fields": {
"destinationLabel",
"destinationPlaceholder",
"phoneLabel",
"phonePlaceholder",
"noteLabel",
"notePlaceholder",
"submitLabel",
"chips",
"isActive",
},
"none_to_empty": set(),
"empty_to_none": {"destinationPlaceholder", "phonePlaceholder", "notePlaceholder"},
"create_defaults": {
"destinationLabel": "目的地/玩法",
"destinationPlaceholder": "例如:贵州、黄果树、西江苗寨",
"phoneLabel": "联系方式",
"phonePlaceholder": "手机号 / 微信号",
"noteLabel": "补充说明",
"notePlaceholder": "出行日期、人数、酒店偏好、预算范围",
"submitLabel": "提交出行需求",
"chips": ["贵州", "黄果树", "荔波小七孔", "西江苗寨", "梵净山", "万峰林"],
},
"ordered": False,
"singleton": True,
},
"vehicleOptions": {
"model": VehicleOption,
"entity": "vehicle_option",
@@ -156,17 +90,6 @@ SITE_CONFIG_MODULES = {
"empty_to_none": {"description", "image"},
"create_defaults": {"description": None, "image": None},
},
"vehicleService": {
"model": VehicleServiceConfig,
"entity": "vehicle_service_config",
"primary": "introTitle",
"fields": {"introTitle", "intro", "serviceSections", "advantages", "processSteps", "isActive"},
"none_to_empty": set(),
"empty_to_none": set(),
"create_defaults": {"intro": "", "serviceSections": [], "advantages": [], "processSteps": []},
"ordered": False,
"singleton": True,
},
}
def media_error(status_code: int, message: str, code: str, details: dict | None = None) -> None:
@@ -542,8 +465,6 @@ def create_home_item(db: Session, request: Request, body, model, entity: str):
audit(db, get_actor_id(request), "create", entity, item.id, after=after)
db.commit()
return success_response(after, status_code=status.HTTP_201_CREATED)
def update_home_item(db: Session, request: Request, item_id: str, body, model, entity: str, label: str, code: str):
item = home_item_or_error(db, model, item_id, label, code)
before = model_dict(item)
@@ -580,52 +501,6 @@ def reorder_home_items(db: Session, request: Request, body: HomeReorderIn, model
return success_response({"items": after})
@router.get("/home/experiences")
def list_home_experiences(_user: AdminUser = Depends(require_admin), db: Session = Depends(get_db)):
return success_response({"items": [model_dict(item) for item in home_items(db, HomeExperience)]})
@router.post("/home/experiences", status_code=status.HTTP_201_CREATED)
def create_home_experience(
body: HomeExperienceCreate,
request: Request,
_user: AdminUser = Depends(require_admin),
db: Session = Depends(get_db),
):
return create_home_item(db, request, body, HomeExperience, "home_experience")
@router.patch("/home/experiences/reorder")
def reorder_home_experiences(
body: HomeReorderIn,
request: Request,
_user: AdminUser = Depends(require_admin),
db: Session = Depends(get_db),
):
return reorder_home_items(db, request, body, HomeExperience, "home_experience")
@router.patch("/home/experiences/{experience_id}")
def update_home_experience(
experience_id: str,
body: HomeExperiencePatch,
request: Request,
_user: AdminUser = Depends(require_admin),
db: Session = Depends(get_db),
):
return update_home_item(db, request, experience_id, body, HomeExperience, "home_experience", "体验推荐", "HOME_EXPERIENCE_NOT_FOUND")
@router.delete("/home/experiences/{experience_id}")
def delete_home_experience(
experience_id: str,
request: Request,
_user: AdminUser = Depends(require_admin),
db: Session = Depends(get_db),
):
return delete_home_item(db, request, experience_id, HomeExperience, "home_experience", "体验推荐", "HOME_EXPERIENCE_NOT_FOUND")
@router.get("/home/team-buildings")
def list_home_team_buildings(_user: AdminUser = Depends(require_admin), db: Session = Depends(get_db)):
return success_response({"items": [model_dict(item) for item in home_items(db, HomeTeamBuilding)]})
@@ -776,8 +651,6 @@ def create_home_wanfa_recommendation(
audit(db, get_actor_id(request), "create", "home_wanfa_recommendation", recommendation.id, after=after)
db.commit()
return success_response(after, status_code=status.HTTP_201_CREATED)
@router.patch("/home/play-recommendations/reorder")
def reorder_home_wanfa_recommendations(
body: HomeReorderIn,
@@ -946,8 +819,6 @@ def create_detail(
audit(db, get_actor_id(request), "create", "detail_record", detail.id, after=after)
db.commit()
return success_response(after, status_code=status.HTTP_201_CREATED)
@router.patch("/details/{detail_id}")
def update_detail(
detail_id: str,
@@ -1420,30 +1291,10 @@ def admin_site_config(_user: AdminUser = Depends(require_admin_permission("admin
hero_slide_admin_dict(item)
for item in scoped_items(db, HeroSlide, _user, HeroSlide.sortOrder.asc())
],
"destinationHero": [
model_dict(item)
for item in scoped_items(db, DestinationHero, _user, DestinationHero.sortOrder.asc())
],
"vehicleOptions": [
model_dict(item)
for item in scoped_items(db, VehicleOption, _user, VehicleOption.sortOrder.asc())
],
"vehicleService": [
model_dict(item)
for item in scoped_items(db, VehicleServiceConfig, _user, VehicleServiceConfig.createdAt.asc())
],
"demandHero": [
model_dict(item)
for item in scoped_items(db, DemandHero, _user, DemandHero.sortOrder.asc())
],
"demandFeatureCards": [
model_dict(item)
for item in scoped_items(db, DemandFeatureCard, _user, DemandFeatureCard.sortOrder.asc())
],
"demandForm": [
model_dict(item)
for item in scoped_items(db, DemandForm, _user, DemandForm.createdAt.asc())
],
}
)
@@ -1557,6 +1408,7 @@ def list_leads(
keyword: str | None = None,
createdFrom: str | None = None,
createdTo: str | None = None,
offset: int = Query(default=0, ge=0),
take: int = Query(default=100, ge=1, le=200),
_user: AdminUser = Depends(require_admin_permission("admin:leads:read")),
db: Session = Depends(get_db),
@@ -1574,6 +1426,7 @@ def list_leads(
select(Lead)
.options(selectinload(Lead.assignedUser))
.order_by(Lead.createdAt.desc())
.offset(offset)
.limit(query.take)
)
if query.status:
@@ -1595,8 +1448,24 @@ def list_leads(
Lead.note.ilike(pattern),
)
).distinct()
leads = db.scalars(apply_data_scope(stmt, Lead, _user, db)).all()
return success_response({"items": [lead_dict(lead) for lead in leads]})
scoped_stmt = apply_data_scope(stmt, Lead, _user, db)
leads = db.scalars(scoped_stmt).all()
count_stmt = select(func.count()).select_from(Lead)
if query.status:
count_stmt = count_stmt.where(Lead.status == query.status)
if query.leadType:
count_stmt = count_stmt.where(Lead.leadType == query.leadType)
if query.sourcePage:
count_stmt = count_stmt.where(Lead.sourcePage == query.sourcePage.strip())
if query.createdFrom:
count_stmt = count_stmt.where(Lead.createdAt >= query.createdFrom)
if query.createdTo:
count_stmt = count_stmt.where(Lead.createdAt < query.createdTo)
if query.keyword and query.keyword.strip():
pattern = f"%{query.keyword.strip()}%"
count_stmt = count_stmt.where(or_(Lead.phone.ilike(pattern), Lead.destination.ilike(pattern), Lead.note.ilike(pattern)))
total = db.scalar(apply_data_scope(count_stmt, Lead, _user, db)) or 0
return success_response({"items": [lead_dict(lead) for lead in leads], "total": total, "offset": offset, "take": query.take})
@router.patch("/leads/{lead_id}/status")
@@ -1646,20 +1515,3 @@ def upload_media_asset(
return success_response(after, status_code=status.HTTP_201_CREATED)
@router.post("/reset-guizhou-content")
def reset_content(request: Request, _user: AdminUser = Depends(require_admin), db: Session = Depends(get_db)):
result = reset_guizhou_content(db)
audit(db, get_actor_id(request), "reset_guizhou_content", "site_content", after=result)
db.commit()
return success_response(result)
@router.post("/publish", status_code=status.HTTP_201_CREATED)
def publish(request: Request, _user: AdminUser = Depends(require_admin), db: Session = Depends(get_db)):
snapshot = site_config(db, active_only=True)
version = SiteVersion(title=f"manual-{utc_now().isoformat()}", status="published", snapshot=snapshot, publishedAt=utc_now())
db.add(version)
db.flush()
audit(db, get_actor_id(request), "publish", "site_version", version.id, model_dict(version))
db.commit()
return success_response(model_dict(version), status_code=status.HTTP_201_CREATED)
+1 -18
View File
@@ -1,6 +1,6 @@
from sqlalchemy import select
from sqlalchemy.orm import Session
from ..models import DemandFeatureCard, DemandForm, DemandHero, DestinationHero, HeroSlide, VehicleOption, VehicleServiceConfig
from ..models import HeroSlide, VehicleOption
from ..serializers import model_dict
@@ -13,32 +13,15 @@ def public_model_dict(item) -> dict:
def site_config(db: Session, active_only: bool, include_public_extras: bool = True) -> dict:
hero_stmt = select(HeroSlide).order_by(HeroSlide.sortOrder.asc())
destination_hero_stmt = select(DestinationHero).order_by(DestinationHero.sortOrder.asc())
vehicle_stmt = select(VehicleOption).order_by(VehicleOption.sortOrder.asc())
demand_hero_stmt = select(DemandHero).order_by(DemandHero.sortOrder.asc())
demand_feature_card_stmt = select(DemandFeatureCard).order_by(DemandFeatureCard.sortOrder.asc())
demand_form_stmt = select(DemandForm).order_by(DemandForm.createdAt.asc())
vehicle_service_stmt = select(VehicleServiceConfig).order_by(VehicleServiceConfig.createdAt.asc())
if active_only:
hero_stmt = hero_stmt.where(HeroSlide.isActive.is_(True))
destination_hero_stmt = destination_hero_stmt.where(DestinationHero.isActive.is_(True))
vehicle_stmt = vehicle_stmt.where(VehicleOption.isActive.is_(True))
demand_hero_stmt = demand_hero_stmt.where(DemandHero.isActive.is_(True))
demand_feature_card_stmt = demand_feature_card_stmt.where(DemandFeatureCard.isActive.is_(True))
demand_form_stmt = demand_form_stmt.where(DemandForm.isActive.is_(True))
vehicle_service_stmt = vehicle_service_stmt.where(VehicleServiceConfig.isActive.is_(True))
hero_slides = db.scalars(hero_stmt).all()
destination_hero = db.scalars(destination_hero_stmt).all()
result = {
"heroSlides": [public_model_dict(item) for item in hero_slides],
"destinationHero": [public_model_dict(item) for item in destination_hero],
}
if include_public_extras:
result["vehicleOptions"] = [public_model_dict(item) for item in db.scalars(vehicle_stmt).all()]
vehicle_service = db.scalars(vehicle_service_stmt).first()
result["vehicleService"] = public_model_dict(vehicle_service) if vehicle_service else None
result["demandHero"] = [public_model_dict(item) for item in db.scalars(demand_hero_stmt).all()]
result["demandFeatureCards"] = [public_model_dict(item) for item in db.scalars(demand_feature_card_stmt).all()]
result["demandForm"] = [public_model_dict(item) for item in db.scalars(demand_form_stmt).all()]
return result
+86 -1
View File
@@ -202,6 +202,27 @@ def update_user(
return {"code": 200, "msg": "success", "data": {"id": user.id}}
@router.delete("/users/{user_id}")
def delete_user(
user_id: str,
request: Request,
current_user: AdminUser = Depends(require_permission("system:user:delete")),
db: Session = Depends(get_db),
store: AdminSessionStore = Depends(get_admin_session_store),
):
user = db.get(AdminUser, user_id)
if not user:
raise HTTPException(status_code=404, detail="管理员不存在")
if user.id == current_user.id:
raise HTTPException(status_code=400, detail="不能删除当前登录账号")
before = model_dict(user)
db.delete(user)
_audit(db, request, "delete", "admin_user", user.id, before=before, after={"id": user.id})
db.commit()
_invalidate_permissions(store, [user.id])
return {"code": 200, "msg": "success", "data": {"id": user.id}}
@router.get("/roles")
def list_roles(_user: AdminUser = Depends(require_permission("system:role:read")), db: Session = Depends(get_db)):
roles = list(db.scalars(select(AdminRole).order_by(AdminRole.createdAt.asc())).all())
@@ -219,7 +240,7 @@ def list_roles(_user: AdminUser = Depends(require_permission("system:role:read")
@router.get("/menus")
def list_menus(_user: AdminUser = Depends(require_permission("system:menu:read")), db: Session = Depends(get_db)):
menus = list(db.scalars(select(AdminMenu).order_by(AdminMenu.sortOrder.asc(), AdminMenu.id.asc())).all())
return {"code": 200, "msg": "success", "data": {"items": [_menu_dict(menu) for menu in menus], "tree": build_menu_tree(menus)}}
return {"code": 200, "msg": "success", "data": {"items": [_menu_dict(menu) for menu in menus], "tree": build_menu_tree(menus, visible_only=False)}}
@router.get("/depts")
@@ -279,6 +300,28 @@ def update_role(role_id: str, body: AdminRolePatch, request: Request, _user: Adm
return {"code": 200, "msg": "success", "data": {"id": role.id}}
@router.delete("/roles/{role_id}")
def delete_role(
role_id: str,
request: Request,
_user: AdminUser = Depends(require_permission("system:role:delete")),
db: Session = Depends(get_db),
store: AdminSessionStore = Depends(get_admin_session_store),
):
role = db.get(AdminRole, role_id)
if not role:
raise HTTPException(status_code=404, detail="角色不存在")
if role.code == "super_admin":
raise HTTPException(status_code=400, detail="系统内置角色不能删除")
before = _role_dict(role)
affected_user_ids = list(db.scalars(select(AdminUserRole.userId).where(AdminUserRole.roleId == role.id)).all())
db.delete(role)
_audit(db, request, "delete", "admin_role", role.id, before=before, after={"id": role.id})
db.commit()
_invalidate_permissions(store, affected_user_ids)
return {"code": 200, "msg": "success", "data": {"id": role.id}}
@router.post("/menus", status_code=status.HTTP_201_CREATED)
def create_menu(body: AdminMenuCreate, request: Request, _user: AdminUser = Depends(require_permission("system:menu:create")), db: Session = Depends(get_db)):
if body.parentId and not db.get(AdminMenu, body.parentId):
@@ -311,6 +354,27 @@ def update_menu(menu_id: str, body: AdminMenuPatch, request: Request, _user: Adm
return {"code": 200, "msg": "success", "data": {"id": menu.id}}
@router.delete("/menus/{menu_id}")
def delete_menu(
menu_id: str,
request: Request,
_user: AdminUser = Depends(require_permission("system:menu:delete")),
db: Session = Depends(get_db),
store: AdminSessionStore = Depends(get_admin_session_store),
):
menu = db.get(AdminMenu, menu_id)
if not menu:
raise HTTPException(status_code=404, detail="菜单不存在")
if db.scalar(select(AdminMenu.id).where(AdminMenu.parentId == menu.id).limit(1)):
raise HTTPException(status_code=409, detail="该菜单存在子菜单,请先删除或移动子菜单")
before = _menu_dict(menu)
db.delete(menu)
_audit(db, request, "delete", "admin_menu", menu.id, before=before, after={"id": menu.id})
db.commit()
_invalidate_permissions(store)
return {"code": 200, "msg": "success", "data": {"id": menu.id}}
@router.post("/depts", status_code=status.HTTP_201_CREATED)
def create_dept(body: AdminDepartmentCreate, request: Request, _user: AdminUser = Depends(require_permission("system:dept:create")), db: Session = Depends(get_db)):
if body.parentId and not db.get(AdminDepartment, body.parentId):
@@ -337,3 +401,24 @@ def update_dept(dept_id: str, body: AdminDepartmentPatch, request: Request, _use
db.commit()
_invalidate_permissions(store)
return {"code": 200, "msg": "success", "data": {"id": dept.id}}
@router.delete("/depts/{dept_id}")
def delete_dept(
dept_id: str,
request: Request,
_user: AdminUser = Depends(require_permission("system:dept:delete")),
db: Session = Depends(get_db),
store: AdminSessionStore = Depends(get_admin_session_store),
):
dept = db.get(AdminDepartment, dept_id)
if not dept:
raise HTTPException(status_code=404, detail="部门不存在")
if db.scalar(select(AdminDepartment.id).where(AdminDepartment.parentId == dept.id).limit(1)):
raise HTTPException(status_code=409, detail="该部门存在子部门,请先删除或移动子部门")
before = _dept_dict(dept)
db.delete(dept)
_audit(db, request, "delete", "admin_department", dept.id, before=before, after={"id": dept.id})
db.commit()
_invalidate_permissions(store)
return {"code": 200, "msg": "success", "data": {"id": dept.id}}
-53
View File
@@ -332,59 +332,6 @@ class DetailPatch(BaseModel):
return normalized or None
class HomeExperienceCreate(BaseModel):
badge: str = Field(min_length=1, max_length=40)
category: str = Field(min_length=1, max_length=80)
title: str = Field(min_length=1, max_length=120)
englishTitle: str = Field(min_length=1, max_length=120)
image: str = Field(min_length=1, max_length=2048)
demandKeyword: str = Field(min_length=1, max_length=120)
isActive: bool = True
sortOrder: int | None = Field(default=None, ge=0)
@field_validator("badge", "category", "title", "englishTitle", "demandKeyword")
@classmethod
def normalize_experience_text(cls, value: str) -> str:
return normalize_required_text(value)
@field_validator("image")
@classmethod
def normalize_experience_image(cls, value: str) -> str:
return normalize_required_url(value)
class HomeExperiencePatch(BaseModel):
badge: str | None = Field(default=None, min_length=1, max_length=40)
category: str | None = Field(default=None, min_length=1, max_length=80)
title: str | None = Field(default=None, min_length=1, max_length=120)
englishTitle: str | None = Field(default=None, min_length=1, max_length=120)
image: str | None = Field(default=None, min_length=1, max_length=2048)
demandKeyword: str | None = Field(default=None, min_length=1, max_length=120)
isActive: bool | None = None
sortOrder: int | None = Field(default=None, ge=0)
@field_validator("badge", "category", "title", "englishTitle", "demandKeyword", mode="before")
@classmethod
def normalize_experience_patch_text(cls, value: str | None) -> str:
if value is None:
raise ValueError("字段不能为空")
return normalize_required_text(value)
@field_validator("image", mode="before")
@classmethod
def normalize_experience_patch_image(cls, value: str | None) -> str:
if value is None:
raise ValueError("字段不能为空")
return normalize_required_url(value)
@field_validator("isActive", "sortOrder", mode="before")
@classmethod
def reject_experience_patch_null(cls, value):
if value is None:
raise ValueError("字段不能为空")
return value
class HomeTeamBuildingCreate(BaseModel):
tag: str = Field(min_length=1, max_length=40)
title: str = Field(min_length=1, max_length=120)
+4 -68
View File
@@ -5,20 +5,13 @@ import json
from sqlalchemy import delete, select
from sqlalchemy.orm import Session
from .auth import hash_password
from .content import DEMAND_FEATURE_CARDS, DEMAND_FORM, DEMAND_HERO, DESTINATION_HERO, HERO_SLIDES, VEHICLE_OPTIONS, VEHICLE_SERVICE
from .content import HERO_SLIDES, VEHICLE_OPTIONS
from .database import Base, SessionLocal, engine
from .models import (
AdminUser,
DemandFeatureCard,
DemandForm,
DemandHero,
DestinationHero,
HeroSlide,
MediaAsset,
SiteVersion,
VehicleOption,
VehicleServiceConfig,
utc_now,
)
def create_media(db: Session, url: str | None, group: str, name: str | None = None) -> None:
if not url:
@@ -33,8 +26,8 @@ def create_media(db: Session, url: str | None, group: str, name: str | None = No
db.add(MediaAsset(url=url, group=group, name=name))
def reset_guizhou_content(db: Session) -> dict:
for model in [SiteVersion, DemandForm, DemandFeatureCard, DemandHero, VehicleOption, VehicleServiceConfig, DestinationHero, HeroSlide, MediaAsset]:
def seed_initial_content(db: Session) -> dict:
for model in [VehicleOption, HeroSlide, MediaAsset]:
db.execute(delete(model))
db.flush()
@@ -50,17 +43,6 @@ def reset_guizhou_content(db: Session) -> dict:
)
)
for index, item in enumerate(DESTINATION_HERO):
create_media(db, item.get("image"), "destination-hero", item["title"])
db.add(
DestinationHero(
title=item["title"],
kicker=item.get("kicker"),
image=item.get("image"),
sortOrder=index,
)
)
for index, option in enumerate(VEHICLE_OPTIONS):
create_media(db, option["image"], "vehicle-option", option["title"])
db.add(
@@ -72,55 +54,9 @@ def reset_guizhou_content(db: Session) -> dict:
)
)
db.add(VehicleServiceConfig(**VEHICLE_SERVICE))
for index, item in enumerate(DEMAND_HERO):
db.add(
DemandHero(
title=item["title"],
kicker=item.get("kicker"),
description=item.get("description"),
steps=item.get("steps", []),
sortOrder=index,
)
)
for index, item in enumerate(DEMAND_FEATURE_CARDS):
db.add(
DemandFeatureCard(
title=item["title"],
description=item.get("description"),
sortOrder=index,
)
)
db.add(DemandForm(**DEMAND_FORM))
snapshot = SiteVersion(
title="guizhou-content-reset",
status="published",
publishedAt=utc_now(),
snapshot={
"heroSlides": len(HERO_SLIDES),
"destinationHero": len(DESTINATION_HERO),
"vehicleOptions": len(VEHICLE_OPTIONS),
"vehicleService": 1,
"demandHero": len(DEMAND_HERO),
"demandFeatureCards": len(DEMAND_FEATURE_CARDS),
"demandForm": 1,
},
)
db.add(snapshot)
db.flush()
return {
"heroSlides": len(HERO_SLIDES),
"destinationHero": len(DESTINATION_HERO),
"vehicleOptions": len(VEHICLE_OPTIONS),
"vehicleService": 1,
"demandHero": len(DEMAND_HERO),
"demandFeatureCards": len(DEMAND_FEATURE_CARDS),
"demandForm": 1,
"siteVersionId": snapshot.id,
}
@@ -142,7 +78,7 @@ def seed_database(reset: bool) -> dict:
role="super_admin",
)
)
result = reset_guizhou_content(db) if reset else {"reset": False}
result = seed_initial_content(db) if reset else {"seeded": False}
db.commit()
return result