- 移除所有废弃的旧首页配置模块,包括destinationHero、demand相关、HomeExperience、vehicleService等表结构与代码,新增数据库迁移删除遗留表 - 重构管理端后台布局为标准RuoYi风格,新增Sidebar、Navbar、SettingsDrawer等组件,替换旧的过渡菜单实现 - 调整移动端响应断点为768px,更新布局相关测试用例,新增布局组件测试 - 删除OperationsTools发布/重置页面,移除/tools菜单入口,清理对应的API调用与测试代码 - 重构玩法模块编辑器:替换旧的图片手动输入框为图片上传组件,移除冗余的取消按钮 - 新增线索查询offset参数支持,完善查询参数处理逻辑 - 优化rbac菜单构建逻辑,新增可见性控制参数 - 修正管家编辑器提示文案,优化系统资源编辑器表单初始化逻辑 - 清理小程序端废弃的体验推荐组件与相关测试代码 - 更新文档与种子脚本,匹配新的API契约与使用说明
153 lines
5.8 KiB
Python
153 lines
5.8 KiB
Python
"""Create independent detail presentation records for frontend pages.
|
|
|
|
Revision ID: 0021_detail_records
|
|
Revises: 0020_home_team_building_details
|
|
"""
|
|
|
|
from datetime import datetime
|
|
|
|
from alembic import context, op
|
|
import sqlalchemy as sa
|
|
from sqlalchemy import inspect
|
|
from sqlalchemy.dialects import postgresql
|
|
|
|
|
|
revision = "0021_detail_records"
|
|
down_revision = "0020_home_team_building_details"
|
|
branch_labels = None
|
|
depends_on = None
|
|
|
|
|
|
DETAIL_RECORD_INDEXES = {
|
|
"ix_DetailRecord_isActive": ["isActive"],
|
|
"ix_DetailRecord_sortOrder": ["sortOrder"],
|
|
}
|
|
|
|
|
|
def missing_detail_record_indexes(existing_indexes: set[str]) -> set[str]:
|
|
return set(DETAIL_RECORD_INDEXES).difference(existing_indexes)
|
|
|
|
|
|
def build_detail_seed_rows(routes, existing_keys: set[str], timestamp: datetime) -> list[dict]:
|
|
return [
|
|
{
|
|
"id": f"detail-{route['id']}",
|
|
"key": route["id"],
|
|
"eyebrow": "玩法推荐",
|
|
"duration": "5天4晚",
|
|
"title": route["title"],
|
|
"subtitle": route["subtitle"],
|
|
"intro": route["subtitle"],
|
|
"highlights": ["核心景观串联", "小团出行,按同行人的节奏灵活调整"],
|
|
"included": ["行程内用车与接送服务"],
|
|
"excluded": ["往返大交通及个人消费"],
|
|
"notes": ["最终方案以出行日期、人数和资源确认结果为准。"],
|
|
"gallery": [route["image"]],
|
|
"isActive": True,
|
|
"sortOrder": route["sortOrder"],
|
|
"createdAt": timestamp,
|
|
"updatedAt": timestamp,
|
|
}
|
|
for route in routes
|
|
if route["id"] not in existing_keys
|
|
]
|
|
|
|
|
|
def is_offline_mode() -> bool:
|
|
try:
|
|
return context.is_offline_mode()
|
|
except (NameError, AttributeError):
|
|
return False
|
|
|
|
|
|
def _create_detail_table() -> None:
|
|
op.create_table(
|
|
"DetailRecord",
|
|
sa.Column("id", sa.String(), nullable=False),
|
|
sa.Column("key", sa.String(), nullable=False),
|
|
sa.Column("eyebrow", sa.String(), nullable=False),
|
|
sa.Column("duration", sa.String(), nullable=False),
|
|
sa.Column("title", sa.String(), nullable=False),
|
|
sa.Column("subtitle", sa.Text(), nullable=False),
|
|
sa.Column("intro", sa.Text(), nullable=False),
|
|
sa.Column("highlights", postgresql.JSONB(astext_type=sa.Text()), nullable=False, server_default=sa.text("'[]'::jsonb")),
|
|
sa.Column("included", postgresql.JSONB(astext_type=sa.Text()), nullable=False, server_default=sa.text("'[]'::jsonb")),
|
|
sa.Column("excluded", postgresql.JSONB(astext_type=sa.Text()), nullable=False, server_default=sa.text("'[]'::jsonb")),
|
|
sa.Column("notes", postgresql.JSONB(astext_type=sa.Text()), nullable=False, server_default=sa.text("'[]'::jsonb")),
|
|
sa.Column("gallery", postgresql.JSONB(astext_type=sa.Text()), nullable=False, server_default=sa.text("'[]'::jsonb")),
|
|
sa.Column("isActive", sa.Boolean(), nullable=False, server_default=sa.text("true")),
|
|
sa.Column("sortOrder", sa.Integer(), nullable=False, server_default="0"),
|
|
sa.Column("createdAt", sa.DateTime(), nullable=False),
|
|
sa.Column("updatedAt", sa.DateTime(), nullable=False),
|
|
sa.PrimaryKeyConstraint("id"),
|
|
sa.UniqueConstraint("key"),
|
|
)
|
|
|
|
|
|
def upgrade() -> None:
|
|
if is_offline_mode():
|
|
table_exists = False
|
|
existing_index_names = set()
|
|
else:
|
|
bind = op.get_bind()
|
|
inspector = inspect(bind)
|
|
table_exists = "DetailRecord" in set(inspector.get_table_names())
|
|
existing_index_names = {
|
|
index["name"]
|
|
for index in inspector.get_indexes("DetailRecord")
|
|
} if table_exists else set()
|
|
|
|
created_table = not table_exists
|
|
if created_table:
|
|
_create_detail_table()
|
|
existing_index_names = set()
|
|
|
|
if created_table:
|
|
for index_name in missing_detail_record_indexes(existing_index_names):
|
|
op.create_index(index_name, "DetailRecord", DETAIL_RECORD_INDEXES[index_name])
|
|
|
|
detail_table = sa.table(
|
|
"DetailRecord",
|
|
sa.column("id", sa.String()),
|
|
sa.column("key", sa.String()),
|
|
sa.column("eyebrow", sa.String()),
|
|
sa.column("duration", sa.String()),
|
|
sa.column("title", sa.String()),
|
|
sa.column("subtitle", sa.Text()),
|
|
sa.column("intro", sa.Text()),
|
|
sa.column("highlights", postgresql.JSONB()),
|
|
sa.column("included", postgresql.JSONB()),
|
|
sa.column("excluded", postgresql.JSONB()),
|
|
sa.column("notes", postgresql.JSONB()),
|
|
sa.column("gallery", postgresql.JSONB()),
|
|
sa.column("isActive", sa.Boolean()),
|
|
sa.column("sortOrder", sa.Integer()),
|
|
sa.column("createdAt", sa.DateTime()),
|
|
sa.column("updatedAt", sa.DateTime()),
|
|
)
|
|
timestamp = datetime(2026, 1, 1)
|
|
if is_offline_mode():
|
|
return
|
|
|
|
conn = op.get_bind()
|
|
routes = conn.execute(
|
|
sa.text('SELECT "id", "title", "subtitle", "image", "sortOrder" FROM "WanfaRoute" ORDER BY "sortOrder" ASC')
|
|
).mappings().all()
|
|
existing_key_result = conn.execute(sa.text('SELECT "key" FROM "DetailRecord"'))
|
|
if hasattr(existing_key_result, "scalars"):
|
|
existing_key_values = existing_key_result.scalars()
|
|
elif hasattr(existing_key_result, "all"):
|
|
existing_key_values = [row[0] for row in existing_key_result.all()]
|
|
else:
|
|
existing_key_values = existing_key_result
|
|
existing_keys = set(existing_key_values)
|
|
seed_rows = build_detail_seed_rows(routes, existing_keys, timestamp)
|
|
if seed_rows:
|
|
op.bulk_insert(detail_table, seed_rows)
|
|
|
|
|
|
def downgrade() -> None:
|
|
op.drop_index("ix_DetailRecord_sortOrder", table_name="DetailRecord")
|
|
op.drop_index("ix_DetailRecord_isActive", table_name="DetailRecord")
|
|
op.drop_table("DetailRecord")
|