- 为后端Admin新增migration_compat工具,支持离线迁移时的基线资源检查,重构全部alembic迁移脚本适配离线模式,避免重复创建已存在的数据库表和字段 - 完善初始数据库迁移脚本,添加默认部门种子数据,新增ConciergeAdvisor详情字段从JSON到JSONB的类型转换迁移 - 小程序端新增图片资源解析工具resolveNetworkImage,实现本地静态资源到CDN的自动映射,添加图片加载容错降级机制 - 完善HeroSlide类型定义,新增actionLabel字段,优化首页轮播组件的文本渲染与错误处理逻辑 - 新增对应测试用例验证迁移路径正确性与前端数据处理逻辑 - 新增临时 brainstorm UI对比草稿与开发临时状态文件
134 lines
4.7 KiB
Python
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")
|