Files
WonderQ-Project/WonderQ-Admin/alembic/versions/0016_concierge_advisors.py
T
duanshuwen 4e04feda5b feat(admin, miniapp): 优化数据库迁移逻辑与小程序图片资源处理
- 为后端Admin新增migration_compat工具,支持离线迁移时的基线资源检查,重构全部alembic迁移脚本适配离线模式,避免重复创建已存在的数据库表和字段
- 完善初始数据库迁移脚本,添加默认部门种子数据,新增ConciergeAdvisor详情字段从JSON到JSONB的类型转换迁移
- 小程序端新增图片资源解析工具resolveNetworkImage,实现本地静态资源到CDN的自动映射,添加图片加载容错降级机制
- 完善HeroSlide类型定义,新增actionLabel字段,优化首页轮播组件的文本渲染与错误处理逻辑
- 新增对应测试用例验证迁移路径正确性与前端数据处理逻辑
- 新增临时 brainstorm UI对比草稿与开发临时状态文件
2026-08-27 11:37:03 +08:00

134 lines
4.7 KiB
Python

"""Create the concierge advisor domain.
Revision ID: 0016_concierge_advisors
Revises: 0015_wanfa
"""
from datetime import datetime
import json
from alembic import context, op
import sqlalchemy as sa
from sqlalchemy import inspect
from app.migration_compat import baseline_table_names
revision = "0016_concierge_advisors"
down_revision = "0015_wanfa"
branch_labels = None
depends_on = None
SEED_ADVISORS = (
{
"id": "advisor-amanda",
"avatar": "https://images.unsplash.com/photo-1494790108377-be9c29b29330?auto=format&fit=crop&fm=jpg&q=80&w=240&h=240",
"name": "Amanda",
"role": "SENIOR TRAVEL ADVISOR",
"details": [
{"icon": "calendar", "label": "服务经验:8年"},
{"icon": "navigate", "label": "擅长领域:瀑降 / 探洞"},
],
"qrImage": "https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=WonderQ%20Concierge",
"isActive": True,
"sortOrder": 0,
},
{
"id": "advisor-mia",
"avatar": "https://images.unsplash.com/photo-1531123897727-8f129e1688ce?auto=format&fit=crop&fm=jpg&q=80&w=240&h=240",
"name": "Mia",
"role": "LIFESTYLE TRAVEL ADVISOR",
"details": [
{"icon": "calendar", "label": "服务经验:6年"},
{"icon": "navigate", "label": "擅长领域:溯溪 / 人文探索"},
],
"qrImage": "https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=WonderQ%20Concierge",
"isActive": True,
"sortOrder": 1,
},
{
"id": "advisor-leo",
"avatar": "https://images.unsplash.com/photo-1500648767791-00dcc994a43e?auto=format&fit=crop&fm=jpg&q=80&w=240&h=240",
"name": "Leo",
"role": "WILD TRAVEL ADVISOR",
"details": [
{"icon": "calendar", "label": "服务经验:9年"},
{"icon": "navigate", "label": "擅长领域:洞穴探险 / 山地徒步"},
],
"qrImage": "https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=WonderQ%20Concierge",
"isActive": True,
"sortOrder": 2,
},
)
def is_offline_mode() -> bool:
try:
return context.is_offline_mode()
except (NameError, AttributeError):
return False
def _seed_advisors() -> None:
offline = is_offline_mode()
details_type = sa.Text() if offline else sa.JSON()
table = sa.table(
"ConciergeAdvisor",
sa.column("id", sa.String()),
sa.column("avatar", sa.String()),
sa.column("name", sa.String()),
sa.column("role", sa.String()),
sa.column("details", details_type),
sa.column("qrImage", sa.String()),
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)
rows = [{**advisor, "createdAt": timestamp, "updatedAt": timestamp} for advisor in SEED_ADVISORS]
if offline:
rows = [{**advisor, "details": json.dumps(advisor["details"], ensure_ascii=False)} for advisor in rows]
op.bulk_insert(table, rows)
def upgrade() -> None:
if is_offline_mode():
existing_tables = baseline_table_names()
else:
bind = op.get_bind()
existing_tables = set(inspect(bind).get_table_names())
if "ConciergeAdvisor" not in existing_tables:
op.create_table(
"ConciergeAdvisor",
sa.Column("id", sa.String(), nullable=False),
sa.Column("avatar", sa.String(), nullable=False),
sa.Column("name", sa.String(), nullable=False),
sa.Column("role", sa.String(), nullable=False),
sa.Column("details", sa.JSON(), nullable=False),
sa.Column("qrImage", sa.String(), nullable=False),
sa.Column("isActive", sa.Boolean(), nullable=False, server_default=sa.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"),
)
op.create_index("ix_ConciergeAdvisor_isActive", "ConciergeAdvisor", ["isActive"])
op.create_index("ix_ConciergeAdvisor_sortOrder", "ConciergeAdvisor", ["sortOrder"])
if is_offline_mode():
_seed_advisors()
return
advisor_table = sa.table("ConciergeAdvisor", sa.column("id", sa.String()))
if bind.execute(sa.select(advisor_table.c.id).limit(1)).first() is None:
_seed_advisors()
def downgrade() -> None:
bind = op.get_bind()
existing_tables = set(inspect(bind).get_table_names())
if "ConciergeAdvisor" in existing_tables:
op.drop_table("ConciergeAdvisor")