refactor: 清理废弃模块并重构管理端与全端代码
- 移除所有废弃的旧首页配置模块,包括destinationHero、demand相关、HomeExperience、vehicleService等表结构与代码,新增数据库迁移删除遗留表 - 重构管理端后台布局为标准RuoYi风格,新增Sidebar、Navbar、SettingsDrawer等组件,替换旧的过渡菜单实现 - 调整移动端响应断点为768px,更新布局相关测试用例,新增布局组件测试 - 删除OperationsTools发布/重置页面,移除/tools菜单入口,清理对应的API调用与测试代码 - 重构玩法模块编辑器:替换旧的图片手动输入框为图片上传组件,移除冗余的取消按钮 - 新增线索查询offset参数支持,完善查询参数处理逻辑 - 优化rbac菜单构建逻辑,新增可见性控制参数 - 修正管家编辑器提示文案,优化系统资源编辑器表单初始化逻辑 - 清理小程序端废弃的体验推荐组件与相关测试代码 - 更新文档与种子脚本,匹配新的API契约与使用说明
This commit is contained in:
1 parent
f6c5ad589f
commit
fbf2c772a8
63 files changed
+2272
-1991
No files matched your search
@@ -53,8 +53,6 @@ SEED_TEAM_BUILDINGS = (
|
||||
"detailParagraphs": ["洞穴、瀑降与协作,适合 10-30 人。"],
|
||||
"image": "https://dimg04.c-ctrip.com/images/1mh0412000njfr1ot9453_W_640_10000.jpg?proc=autoorient",
|
||||
"demandKeyword": "户外团建",
|
||||
"detailSubtitle": "洞穴、瀑降与协作,适合 10-30 人。",
|
||||
"detailParagraphs": ["洞穴、瀑降与协作,适合 10-30 人。"],
|
||||
"isActive": True,
|
||||
"sortOrder": 0,
|
||||
},
|
||||
@@ -67,8 +65,6 @@ SEED_TEAM_BUILDINGS = (
|
||||
"detailParagraphs": ["溯溪与分组协作,兼顾参与感与安全。"],
|
||||
"image": "https://dimg04.c-ctrip.com/images/0EQ5712000ca7t504EC0E_W_640_10000.jpg?proc=autoorient",
|
||||
"demandKeyword": "峡谷团建",
|
||||
"detailSubtitle": "溯溪与分组协作,兼顾参与感与安全。",
|
||||
"detailParagraphs": ["溯溪与分组协作,兼顾参与感与安全。"],
|
||||
"isActive": True,
|
||||
"sortOrder": 1,
|
||||
},
|
||||
@@ -81,8 +77,6 @@ SEED_TEAM_BUILDINGS = (
|
||||
"detailParagraphs": ["夜游、长桌宴与文化体验,适合团建收尾。"],
|
||||
"image": "https://p6.itc.cn/q_70/images03/20200918/df728d2b79d943da869333e2ea2c92c8.jpeg",
|
||||
"demandKeyword": "贵州团建",
|
||||
"detailSubtitle": "夜游、长桌宴与文化体验,适合团建收尾。",
|
||||
"detailParagraphs": ["夜游、长桌宴与文化体验,适合团建收尾。"],
|
||||
"isActive": True,
|
||||
"sortOrder": 2,
|
||||
},
|
||||
@@ -95,6 +89,14 @@ OPTIONAL_SEED_COLUMN_TYPES = {
|
||||
"images": postgresql.JSONB(astext_type=sa.Text()),
|
||||
}
|
||||
|
||||
|
||||
def is_offline_mode() -> bool:
|
||||
"""Keep migration helpers directly testable outside Alembic's context proxy."""
|
||||
try:
|
||||
return context.is_offline_mode()
|
||||
except (NameError, AttributeError):
|
||||
return False
|
||||
|
||||
SEED_WILD_ARCHIVES = (
|
||||
{
|
||||
"id": "hundred-meter-descent",
|
||||
@@ -112,7 +114,6 @@ SEED_WILD_ARCHIVES = (
|
||||
"title": "石龙洞",
|
||||
"image": "https://www.zurnal24.si/media/img/5e/d5/9526a56dba168aa136f3.jpeg",
|
||||
"images": ["https://www.zurnal24.si/media/img/5e/d5/9526a56dba168aa136f3.jpeg"],
|
||||
"images": ["https://www.zurnal24.si/media/img/5e/d5/9526a56dba168aa136f3.jpeg"],
|
||||
"demandKeyword": "地心探险",
|
||||
"isActive": True,
|
||||
"sortOrder": 1,
|
||||
@@ -122,7 +123,6 @@ SEED_WILD_ARCHIVES = (
|
||||
"title": "绝壁迎流",
|
||||
"image": "https://q9.itc.cn/q_70/images03/20250810/b62f5afc191a4947a66e6b32721b0235.jpeg",
|
||||
"images": ["https://q9.itc.cn/q_70/images03/20250810/b62f5afc191a4947a66e6b32721b0235.jpeg"],
|
||||
"images": ["https://q9.itc.cn/q_70/images03/20250810/b62f5afc191a4947a66e6b32721b0235.jpeg"],
|
||||
"demandKeyword": "峡谷探险",
|
||||
"isActive": True,
|
||||
"sortOrder": 2,
|
||||
@@ -155,23 +155,37 @@ def prepare_seed_rows(
|
||||
available_columns: set[str],
|
||||
timestamp: datetime,
|
||||
) -> list[dict]:
|
||||
return [
|
||||
{
|
||||
key: value
|
||||
for key, value in {**row, "createdAt": timestamp, "updatedAt": timestamp}.items()
|
||||
if key in available_columns
|
||||
}
|
||||
for row in rows
|
||||
]
|
||||
prepared_rows = []
|
||||
for row in rows:
|
||||
values = {**row, "createdAt": timestamp, "updatedAt": timestamp}
|
||||
if "detailSubtitle" in available_columns and not values.get("detailSubtitle"):
|
||||
values["detailSubtitle"] = values.get("description", "")
|
||||
if "detailParagraphs" in available_columns and not values.get("detailParagraphs"):
|
||||
values["detailParagraphs"] = [values.get("description", "")]
|
||||
if "images" in available_columns and not values.get("images"):
|
||||
values["images"] = [values.get("image", "")]
|
||||
prepared_rows.append(
|
||||
{key: value for key, value in values.items() if key in available_columns}
|
||||
)
|
||||
return prepared_rows
|
||||
|
||||
|
||||
# Kept for compatibility with the bootstrap migration tests and older tooling.
|
||||
_prepare_seed_rows = prepare_seed_rows
|
||||
|
||||
|
||||
def _seed_table(table_name: str, columns: list[sa.Column], rows: tuple[dict, ...]) -> None:
|
||||
column_types = {column.name: column.type for column in columns}
|
||||
if context.is_offline_mode():
|
||||
if is_offline_mode():
|
||||
available_columns = set(column_types)
|
||||
else:
|
||||
bind = op.get_bind()
|
||||
available_columns = {column["name"] for column in inspect(bind).get_columns(table_name)}
|
||||
current_columns = inspect(bind).get_columns(table_name)
|
||||
available_columns = {column["name"] for column in current_columns}
|
||||
column_types = {
|
||||
column["name"]: column.get("type", sa.String())
|
||||
for column in current_columns
|
||||
}
|
||||
for name, column_type in OPTIONAL_SEED_COLUMN_TYPES.items():
|
||||
if name in available_columns:
|
||||
column_types.setdefault(name, column_type)
|
||||
@@ -230,7 +244,7 @@ def upgrade() -> None:
|
||||
_create_table("HomeTeamBuilding", team_columns)
|
||||
_create_table("HomeWildArchive", archive_columns)
|
||||
|
||||
if context.is_offline_mode():
|
||||
if is_offline_mode():
|
||||
_seed_table("HomeExperience", experience_columns, SEED_EXPERIENCES)
|
||||
_seed_table("HomeTeamBuilding", team_columns, SEED_TEAM_BUILDINGS)
|
||||
_seed_table("HomeWildArchive", archive_columns, SEED_WILD_ARCHIVES)
|
||||
|
||||
@@ -7,7 +7,6 @@ Revises: 0018_home_wanfa_recommendations
|
||||
from alembic import context, op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import inspect
|
||||
from sqlalchemy import inspect
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
|
||||
@@ -21,8 +20,15 @@ def should_add_images_column(existing_columns: set[str]) -> bool:
|
||||
return "images" not in existing_columns
|
||||
|
||||
|
||||
def is_offline_mode() -> bool:
|
||||
try:
|
||||
return context.is_offline_mode()
|
||||
except (NameError, AttributeError):
|
||||
return False
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
if context.is_offline_mode():
|
||||
if is_offline_mode():
|
||||
add_images_column = True
|
||||
else:
|
||||
bind = op.get_bind()
|
||||
|
||||
@@ -7,7 +7,6 @@ Revises: 0019_home_wild_archive_images
|
||||
from alembic import context, op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import inspect
|
||||
from sqlalchemy import inspect
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
|
||||
@@ -24,8 +23,15 @@ def missing_detail_columns(existing_columns: set[str]) -> set[str]:
|
||||
return DETAIL_COLUMNS.difference(existing_columns)
|
||||
|
||||
|
||||
def is_offline_mode() -> bool:
|
||||
try:
|
||||
return context.is_offline_mode()
|
||||
except (NameError, AttributeError):
|
||||
return False
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
if context.is_offline_mode():
|
||||
if is_offline_mode():
|
||||
columns_to_add = DETAIL_COLUMNS
|
||||
else:
|
||||
bind = op.get_bind()
|
||||
|
||||
@@ -9,7 +9,6 @@ from datetime import datetime
|
||||
from alembic import context, op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import inspect
|
||||
from sqlalchemy import inspect
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
|
||||
@@ -54,8 +53,39 @@ def build_detail_seed_rows(routes, existing_keys: set[str], timestamp: datetime)
|
||||
]
|
||||
|
||||
|
||||
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 context.is_offline_mode():
|
||||
if is_offline_mode():
|
||||
table_exists = False
|
||||
existing_index_names = set()
|
||||
else:
|
||||
@@ -67,38 +97,14 @@ def upgrade() -> None:
|
||||
for index in inspector.get_indexes("DetailRecord")
|
||||
} if table_exists else set()
|
||||
|
||||
if not table_exists:
|
||||
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"),
|
||||
)
|
||||
created_table = not table_exists
|
||||
if created_table:
|
||||
_create_detail_table()
|
||||
existing_index_names = set()
|
||||
|
||||
for index_name in missing_detail_record_indexes(existing_index_names):
|
||||
op.create_index(index_name, "DetailRecord", DETAIL_RECORD_INDEXES[index_name])
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
bind = op.get_bind()
|
||||
if "DetailRecord" not in set(inspect(bind).get_table_names()):
|
||||
_create_detail_table()
|
||||
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",
|
||||
@@ -120,14 +126,21 @@ def upgrade() -> None:
|
||||
sa.column("updatedAt", sa.DateTime()),
|
||||
)
|
||||
timestamp = datetime(2026, 1, 1)
|
||||
if context.is_offline_mode():
|
||||
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_keys = set(conn.execute(sa.text('SELECT "key" FROM "DetailRecord"')).scalars())
|
||||
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)
|
||||
|
||||
@@ -7,7 +7,6 @@ Revises: 0022_opaque_ids
|
||||
from alembic import context, op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import inspect
|
||||
from sqlalchemy import inspect
|
||||
|
||||
|
||||
revision = "0023_detail_concierge_advisor"
|
||||
@@ -20,8 +19,15 @@ def should_add_concierge_advisor_column(existing_columns: set[str]) -> bool:
|
||||
return "conciergeAdvisorId" not in existing_columns
|
||||
|
||||
|
||||
def is_offline_mode() -> bool:
|
||||
try:
|
||||
return context.is_offline_mode()
|
||||
except (NameError, AttributeError):
|
||||
return False
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
if context.is_offline_mode():
|
||||
if is_offline_mode():
|
||||
should_add = True
|
||||
else:
|
||||
existing_columns = {
|
||||
@@ -35,7 +41,7 @@ def upgrade() -> None:
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
if context.is_offline_mode():
|
||||
if is_offline_mode():
|
||||
op.drop_column("DetailRecord", "conciergeAdvisorId")
|
||||
return
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
from alembic import context, op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import inspect
|
||||
from sqlalchemy import inspect
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
|
||||
@@ -47,8 +46,15 @@ def should_create_customer_fk(foreign_keys: list[dict]) -> bool:
|
||||
)
|
||||
|
||||
|
||||
def is_offline_mode() -> bool:
|
||||
try:
|
||||
return context.is_offline_mode()
|
||||
except (NameError, AttributeError):
|
||||
return False
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
if context.is_offline_mode():
|
||||
if is_offline_mode():
|
||||
existing_lead_columns = set()
|
||||
existing_lead_indexes = set()
|
||||
existing_lead_foreign_keys = []
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
"""Remove superseded homepage configuration modules.
|
||||
|
||||
The homepage workbench now owns the five supported areas: hero slides,
|
||||
wanfa recommendations, vehicle options, team buildings, and wild archives.
|
||||
The former destination/demand/experience singleton tables are no longer part
|
||||
of the public or admin contracts and must not be recreated by ``create_all``.
|
||||
"""
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision = "0027_remove_legacy_home_modules"
|
||||
down_revision = "0026_admin_ownership"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
LEGACY_TABLES = (
|
||||
"DemandForm",
|
||||
"DemandFeatureCard",
|
||||
"DemandHero",
|
||||
"DestinationHero",
|
||||
"HomeExperience",
|
||||
"VehicleServiceConfig",
|
||||
)
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
for table_name in LEGACY_TABLES:
|
||||
op.execute(sa.text(f'DROP TABLE IF EXISTS "{table_name}"'))
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# The legacy tables were removed from the ORM and contracts. Recreating
|
||||
# them on downgrade would produce empty, unsupported modules, so the
|
||||
# destructive cleanup intentionally has no reverse operation.
|
||||
pass
|
||||
@@ -0,0 +1,19 @@
|
||||
"""Remove the obsolete site version snapshot table."""
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision = "0033_remove_site_versions"
|
||||
down_revision = "0027_remove_legacy_home_modules"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.execute(sa.text('DROP TABLE IF EXISTS "SiteVersion"'))
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# Site snapshots were an obsolete release/reset mechanism and are not restored.
|
||||
pass
|
||||
Reference in new issue
Block a user