feat: 新增客片案例与团队共创模块,重构玩法与配置
- 新增客片案例全流程功能,包含前台页面、管理端配置、后端API、数据库迁移与文档 - 新增团队共创详情页面与对应接口 - 重构玩法模块:将静态playData替换为动态API获取,拆分类型定义到playTypes.ts - 优化vite构建配置与环境变量处理,调整详情页操作栏文案 - 更新全量相关文档与配图,新增客片案例、团队共创API文档 - 新增测试用例,完善数据归一化逻辑 - 调整路由配置与环境变量示例文件
This commit is contained in:
1 parent
cda8069630
commit
7ba92f3c5d
76 files changed
+7970
-137
No files matched your search
@@ -6,6 +6,7 @@ Revises: 0013_remove_map_theme_modules
|
||||
|
||||
from alembic import op
|
||||
from sqlalchemy import inspect
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision = "0014_remove_destination_regions_module"
|
||||
@@ -15,6 +16,13 @@ depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.alter_column(
|
||||
"alembic_version",
|
||||
"version_num",
|
||||
existing_type=sa.String(length=32),
|
||||
type_=sa.String(length=128),
|
||||
existing_nullable=False,
|
||||
)
|
||||
existing_tables = set(inspect(op.get_bind()).get_table_names())
|
||||
if "DestinationRegion" in existing_tables:
|
||||
op.drop_table("DestinationRegion")
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
"""Create the wanfa category and route domain.
|
||||
|
||||
Revision ID: 0015_wanfa
|
||||
Revises: 0014_remove_destination_regions_module
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from alembic import context, op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import inspect
|
||||
|
||||
|
||||
revision = "0015_wanfa"
|
||||
down_revision = "0014_remove_destination_regions_module"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
SEED_CATEGORIES = (
|
||||
("family-route", "亲子路线", 0),
|
||||
("photo-route", "旅拍路线", 1),
|
||||
("healing-route", "疗愈路线", 2),
|
||||
("team-building", "团建", 3),
|
||||
("guizhou-panorama", "贵州全景", 4),
|
||||
("private-custom", "私人定制", 5),
|
||||
)
|
||||
|
||||
SEED_ROUTES = (
|
||||
("family-water", "family-route", "亲子玩水", "贵州·轻松节奏与自然课堂", "https://p0.51img.ca/i/669740a019b94.jpeg", 4, "亲子玩水", 0),
|
||||
("family-village", "family-route", "亲子村寨漫游", "贵州·苗寨、古城与在地体验", "https://p6.itc.cn/q_70/images03/20200918/df728d2b79d943da869333e2ea2c92c8.jpeg", 3, "亲子村寨漫游", 1),
|
||||
("family-grassland", "family-route", "高原亲子假期", "贵州·草甸、花海与轻徒步", "https://p2.cri.cn/M00/89/21/rBABC2aHnWeACxj5AAAAAAAAAAA159.1000x566.jpg", 2, "高原亲子假期", 2),
|
||||
("miao-photo", "photo-route", "苗寨人文旅拍", "黔东南·村寨与非遗影像", "https://i.pinimg.com/736x/d5/86/5d/d5865d0b2765dfb6713787aede09f086.jpg", 3, "苗寨旅拍", 0),
|
||||
("peak-photo", "photo-route", "峰林风光旅拍", "黔西南·峰林与峡谷取景", "https://wanfenglin-guanwang.oss-cn-chengdu.aliyuncs.com/uploads/20240718/23ea4291a9570b94a1f8df9a012c18ec.jpg", 2, "万峰林旅拍", 1),
|
||||
("terrace-photo", "photo-route", "梯田日落旅拍", "黔东南·侗寨与山野光影", "https://news.cgtn.com/news/2023-08-28/Spectacular-rice-terraces-in-Guizhou-1mDHGG4vtHq/img/9ff1dc49c6304b35bdb83bc1f7f8df93/9ff1dc49c6304b35bdb83bc1f7f8df93.jpeg", 2, "梯田旅拍", 2),
|
||||
("mountain-healing", "healing-route", "山野慢游", "贵州·森林、溪谷与松弛时光", "https://images.unsplash.com/photo-1501785888041-af3ef285b470?auto=format&fit=crop&fm=jpg&q=80&w=1200&h=800", 3, "山野慢游", 0),
|
||||
("hot-spring-healing", "healing-route", "温泉与村寨", "黔东南·泡汤、慢行与在地风味", "https://www.gz007.net/images/upload/jianhewenquan2.jpg", 2, "温泉与村寨", 1),
|
||||
("river-healing", "healing-route", "清凉河谷", "荔波·水线轻徒步与自然疗愈", "https://p0.51img.ca/i/669740a019b94.jpeg", 3, "清凉河谷", 2),
|
||||
("team-challenge", "team-building", "山野挑战", "贵州·峡谷协作与户外任务", "https://q9.itc.cn/q_70/images03/20250810/b62f5afc191a4947a66e6b32721b0235.jpeg", 3, "山野挑战", 0),
|
||||
("team-stream", "team-building", "溪谷共创", "贵州·溯溪、协作与团队复盘", "https://dimg04.c-ctrip.com/images/0EQ5712000ca7t504EC0E_W_640_10000.jpg?proc=autoorient", 2, "溪谷共创", 1),
|
||||
("team-culture", "team-building", "苗寨共聚", "黔东南·人文体验与团队共创", "https://news.cgtn.com/news/2023-07-13/Miao-people-celebrate-Canoe-Dragon-Boat-Festival-1lpanidzxu0/img/61b7100caa8a441aacb3780cfa3eeb69/61b7100caa8a441aacb3780cfa3eeb69.jpeg", 2, "苗寨共聚", 2),
|
||||
("classic-panorama", "guizhou-panorama", "经典贵州全景", "贵州·瀑布、苗寨、古城与梵净山", "https://news.cgtn.com/news/2023-07-02/Picturesque-landscapes-draw-tourists-to-Guizhou--1l6RJMNVStq/img/f0d32df3426e4b70bf7a8b00a3404ef9/f0d32df3426e4b70bf7a8b00a3404ef9.jpeg", 8, "经典人文打卡线路", 0),
|
||||
("mountain-panorama", "guizhou-panorama", "山水人文全景", "贵州·小七孔、侗寨与山地风光", "https://p6.itc.cn/q_70/images03/20200918/df728d2b79d943da869333e2ea2c92c8.jpeg", 6, "贵州全景", 1),
|
||||
("wild-panorama", "guizhou-panorama", "山野户外全景", "贵州·溶洞、峰林与野咖路线", "https://wanfenglin-guanwang.oss-cn-chengdu.aliyuncs.com/uploads/20240718/23ea4291a9570b94a1f8df9a012c18ec.jpg", 7, "山野户外全景", 2),
|
||||
("private-family", "private-custom", "家庭专属定制", "贵州·按同行人和节奏安排", "https://news.cgtn.com/news/2023-06-23/Zhenyuan-ancient-town-a-must-see-in-Guizhou-1kRXUSXOP1m/img/6aead33b391640589740a7672c66a685/6aead33b391640589740a7672c66a685.jpeg", 1, "家庭专属定制", 0),
|
||||
("private-business", "private-custom", "企业专属定制", "贵州·团建、会议与奖励旅行", "https://wanfenglin-guanwang.oss-cn-chengdu.aliyuncs.com/uploads/20240718/23ea4291a9570b94a1f8df9a012c18ec.jpg", 1, "企业专属定制", 1),
|
||||
("private-wild", "private-custom", "山野深度定制", "贵州·溶洞、瀑降与小众目的地", "https://www.zurnal24.si/media/img/5e/d5/9526a56dba168aa136f3.jpeg", 1, "山野深度定制", 2),
|
||||
)
|
||||
|
||||
|
||||
def _seed_wanfa() -> None:
|
||||
category_table = sa.table(
|
||||
"WanfaCategory",
|
||||
sa.column("id", sa.String()),
|
||||
sa.column("label", sa.String()),
|
||||
sa.column("sortOrder", sa.Integer()),
|
||||
sa.column("createdAt", sa.DateTime()),
|
||||
sa.column("updatedAt", sa.DateTime()),
|
||||
)
|
||||
route_table = sa.table(
|
||||
"WanfaRoute",
|
||||
sa.column("id", sa.String()),
|
||||
sa.column("categoryId", sa.String()),
|
||||
sa.column("title", sa.String()),
|
||||
sa.column("subtitle", sa.Text()),
|
||||
sa.column("image", sa.String()),
|
||||
sa.column("routeCount", sa.Integer()),
|
||||
sa.column("demandKeyword", sa.String()),
|
||||
sa.column("sortOrder", sa.Integer()),
|
||||
sa.column("createdAt", sa.DateTime()),
|
||||
sa.column("updatedAt", sa.DateTime()),
|
||||
)
|
||||
timestamp = datetime(2026, 1, 1)
|
||||
op.bulk_insert(
|
||||
category_table,
|
||||
[
|
||||
{"id": item_id, "label": label, "sortOrder": sort_order, "createdAt": timestamp, "updatedAt": timestamp}
|
||||
for item_id, label, sort_order in SEED_CATEGORIES
|
||||
],
|
||||
)
|
||||
op.bulk_insert(
|
||||
route_table,
|
||||
[
|
||||
{
|
||||
"id": item_id,
|
||||
"categoryId": category_id,
|
||||
"title": title,
|
||||
"subtitle": subtitle,
|
||||
"image": image,
|
||||
"routeCount": route_count,
|
||||
"demandKeyword": demand_keyword,
|
||||
"sortOrder": sort_order,
|
||||
"createdAt": timestamp,
|
||||
"updatedAt": timestamp,
|
||||
}
|
||||
for item_id, category_id, title, subtitle, image, route_count, demand_keyword, sort_order in SEED_ROUTES
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
bind = op.get_bind()
|
||||
existing_tables = set(inspect(bind).get_table_names())
|
||||
|
||||
if "WanfaCategory" not in existing_tables:
|
||||
op.create_table(
|
||||
"WanfaCategory",
|
||||
sa.Column("id", sa.String(), nullable=False),
|
||||
sa.Column("label", sa.String(), nullable=False),
|
||||
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_WanfaCategory_sortOrder", "WanfaCategory", ["sortOrder"])
|
||||
|
||||
if "WanfaRoute" not in existing_tables:
|
||||
op.create_table(
|
||||
"WanfaRoute",
|
||||
sa.Column("id", sa.String(), nullable=False),
|
||||
sa.Column("categoryId", sa.String(), nullable=False),
|
||||
sa.Column("title", sa.String(), nullable=False),
|
||||
sa.Column("subtitle", sa.Text(), nullable=False),
|
||||
sa.Column("image", sa.String(), nullable=False),
|
||||
sa.Column("routeCount", sa.Integer(), nullable=False),
|
||||
sa.Column("demandKeyword", sa.String(), nullable=False),
|
||||
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.ForeignKeyConstraint(["categoryId"], ["WanfaCategory.id"], ondelete="RESTRICT"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_index("ix_WanfaRoute_categoryId", "WanfaRoute", ["categoryId"])
|
||||
op.create_index("ix_WanfaRoute_sortOrder", "WanfaRoute", ["sortOrder"])
|
||||
|
||||
if context.is_offline_mode():
|
||||
_seed_wanfa()
|
||||
return
|
||||
|
||||
category_table = sa.table("WanfaCategory", sa.column("id", sa.String()))
|
||||
route_table = sa.table("WanfaRoute", sa.column("id", sa.String()))
|
||||
category_exists = bind.execute(sa.select(category_table.c.id).limit(1)).first() is not None
|
||||
route_exists = bind.execute(sa.select(route_table.c.id).limit(1)).first() is not None
|
||||
if not category_exists and not route_exists:
|
||||
_seed_wanfa()
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
bind = op.get_bind()
|
||||
existing_tables = set(inspect(bind).get_table_names())
|
||||
if "WanfaRoute" in existing_tables:
|
||||
op.drop_table("WanfaRoute")
|
||||
if "WanfaCategory" in existing_tables:
|
||||
op.drop_table("WanfaCategory")
|
||||
@@ -0,0 +1,118 @@
|
||||
"""Create the concierge advisor domain.
|
||||
|
||||
Revision ID: 0016_concierge_advisors
|
||||
Revises: 0015_wanfa
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from alembic import context, op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import inspect
|
||||
|
||||
|
||||
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 _seed_advisors() -> None:
|
||||
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", sa.JSON()),
|
||||
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)
|
||||
op.bulk_insert(
|
||||
table,
|
||||
[{**advisor, "createdAt": timestamp, "updatedAt": timestamp} for advisor in SEED_ADVISORS],
|
||||
)
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
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 context.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")
|
||||
@@ -0,0 +1,195 @@
|
||||
"""Create the homepage content domains.
|
||||
|
||||
Revision ID: 0017_home_content
|
||||
Revises: 0016_concierge_advisors
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from alembic import context, op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import inspect
|
||||
|
||||
|
||||
revision = "0017_home_content"
|
||||
down_revision = "0016_concierge_advisors"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
SEED_EXPERIENCES = (
|
||||
{
|
||||
"id": "waterfall-descent",
|
||||
"badge": "玩过推荐",
|
||||
"category": "瀑降体验",
|
||||
"title": "悬崖瀑降",
|
||||
"englishTitle": "WATERFALL DESCENT",
|
||||
"image": "https://genk.mediacdn.vn/139269124445442048/2024/4/27/10-23-sinkhole-1714189653945948438879.jpg",
|
||||
"demandKeyword": "悬崖瀑降",
|
||||
"isActive": True,
|
||||
"sortOrder": 0,
|
||||
},
|
||||
{
|
||||
"id": "cave-exploration",
|
||||
"badge": "玩过推荐",
|
||||
"category": "地心探险",
|
||||
"title": "森林&探洞",
|
||||
"englishTitle": "CAVE EXPLORATION",
|
||||
"image": "https://www.zurnal24.si/media/img/5e/d5/9526a56dba168aa136f3.jpeg",
|
||||
"demandKeyword": "地心探险",
|
||||
"isActive": True,
|
||||
"sortOrder": 1,
|
||||
},
|
||||
)
|
||||
|
||||
SEED_TEAM_BUILDINGS = (
|
||||
{
|
||||
"id": "wild-challenge",
|
||||
"tag": "户外挑战",
|
||||
"title": "山野挑战,共创极境",
|
||||
"description": "洞穴、瀑降与协作,适合 10-30 人。",
|
||||
"image": "https://dimg04.c-ctrip.com/images/1mh0412000njfr1ot9453_W_640_10000.jpg?proc=autoorient",
|
||||
"demandKeyword": "户外团建",
|
||||
"isActive": True,
|
||||
"sortOrder": 0,
|
||||
},
|
||||
{
|
||||
"id": "canyon-teamwork",
|
||||
"tag": "团队协作",
|
||||
"title": "峡谷溯溪,默契同行",
|
||||
"description": "溯溪与分组协作,兼顾参与感与安全。",
|
||||
"image": "https://dimg04.c-ctrip.com/images/0EQ5712000ca7t504EC0E_W_640_10000.jpg?proc=autoorient",
|
||||
"demandKeyword": "峡谷团建",
|
||||
"isActive": True,
|
||||
"sortOrder": 1,
|
||||
},
|
||||
{
|
||||
"id": "village-gathering",
|
||||
"tag": "人文聚会",
|
||||
"title": "苗寨共聚,认识彼此",
|
||||
"description": "夜游、长桌宴与文化体验,适合团建收尾。",
|
||||
"image": "https://p6.itc.cn/q_70/images03/20200918/df728d2b79d943da869333e2ea2c92c8.jpeg",
|
||||
"demandKeyword": "贵州团建",
|
||||
"isActive": True,
|
||||
"sortOrder": 2,
|
||||
},
|
||||
)
|
||||
|
||||
SEED_WILD_ARCHIVES = (
|
||||
{
|
||||
"id": "hundred-meter-descent",
|
||||
"title": "百米自降",
|
||||
"image": "https://genk.mediacdn.vn/139269124445442048/2024/4/27/10-23-sinkhole-1714189653945948438879.jpg",
|
||||
"demandKeyword": "悬崖瀑降",
|
||||
"isActive": True,
|
||||
"sortOrder": 0,
|
||||
},
|
||||
{
|
||||
"id": "shilong-cave",
|
||||
"title": "石龙洞",
|
||||
"image": "https://www.zurnal24.si/media/img/5e/d5/9526a56dba168aa136f3.jpeg",
|
||||
"demandKeyword": "地心探险",
|
||||
"isActive": True,
|
||||
"sortOrder": 1,
|
||||
},
|
||||
{
|
||||
"id": "cliff-current",
|
||||
"title": "绝壁迎流",
|
||||
"image": "https://q9.itc.cn/q_70/images03/20250810/b62f5afc191a4947a66e6b32721b0235.jpeg",
|
||||
"demandKeyword": "峡谷探险",
|
||||
"isActive": True,
|
||||
"sortOrder": 2,
|
||||
},
|
||||
{
|
||||
"id": "canyon-streaming",
|
||||
"title": "峡谷溯溪",
|
||||
"image": "https://dimg04.c-ctrip.com/images/0EQ5712000ca7t504EC0E_W_640_10000.jpg?proc=autoorient",
|
||||
"demandKeyword": "峡谷溯溪",
|
||||
"isActive": True,
|
||||
"sortOrder": 3,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def _create_table(table_name: str, columns: list[sa.Column]) -> None:
|
||||
bind = op.get_bind()
|
||||
if table_name in set(inspect(bind).get_table_names()):
|
||||
return
|
||||
op.create_table(table_name, *columns, sa.PrimaryKeyConstraint("id"))
|
||||
op.create_index(f"ix_{table_name}_isActive", table_name, ["isActive"])
|
||||
op.create_index(f"ix_{table_name}_sortOrder", table_name, ["sortOrder"])
|
||||
|
||||
|
||||
def _seed_table(table_name: str, columns: list[sa.Column], rows: tuple[dict, ...]) -> None:
|
||||
table = sa.table(table_name, *[sa.column(column.name, column.type) for column in columns])
|
||||
timestamp = datetime(2026, 1, 1)
|
||||
op.bulk_insert(
|
||||
table,
|
||||
[{**row, "createdAt": timestamp, "updatedAt": timestamp} for row in rows],
|
||||
)
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
experience_columns = [
|
||||
sa.Column("id", sa.String(), nullable=False),
|
||||
sa.Column("badge", sa.String(), nullable=False),
|
||||
sa.Column("category", sa.String(), nullable=False),
|
||||
sa.Column("title", sa.String(), nullable=False),
|
||||
sa.Column("englishTitle", sa.String(), nullable=False),
|
||||
sa.Column("image", sa.String(), nullable=False),
|
||||
sa.Column("demandKeyword", 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),
|
||||
]
|
||||
team_columns = [
|
||||
sa.Column("id", sa.String(), nullable=False),
|
||||
sa.Column("tag", sa.String(), nullable=False),
|
||||
sa.Column("title", sa.String(), nullable=False),
|
||||
sa.Column("description", sa.Text(), nullable=False),
|
||||
sa.Column("image", sa.String(), nullable=False),
|
||||
sa.Column("demandKeyword", 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),
|
||||
]
|
||||
archive_columns = [
|
||||
sa.Column("id", sa.String(), nullable=False),
|
||||
sa.Column("title", sa.String(), nullable=False),
|
||||
sa.Column("image", sa.String(), nullable=False),
|
||||
sa.Column("demandKeyword", 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),
|
||||
]
|
||||
|
||||
_create_table("HomeExperience", experience_columns)
|
||||
_create_table("HomeTeamBuilding", team_columns)
|
||||
_create_table("HomeWildArchive", archive_columns)
|
||||
|
||||
if context.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)
|
||||
return
|
||||
|
||||
bind = op.get_bind()
|
||||
for table_name, columns, rows in (
|
||||
("HomeExperience", experience_columns, SEED_EXPERIENCES),
|
||||
("HomeTeamBuilding", team_columns, SEED_TEAM_BUILDINGS),
|
||||
("HomeWildArchive", archive_columns, SEED_WILD_ARCHIVES),
|
||||
):
|
||||
table = sa.table(table_name, sa.column("id", sa.String()))
|
||||
if bind.execute(sa.select(table.c.id).limit(1)).first() is None:
|
||||
_seed_table(table_name, columns, rows)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
bind = op.get_bind()
|
||||
existing_tables = set(inspect(bind).get_table_names())
|
||||
for table_name in ("HomeWildArchive", "HomeTeamBuilding", "HomeExperience"):
|
||||
if table_name in existing_tables:
|
||||
op.drop_table(table_name)
|
||||
@@ -0,0 +1,43 @@
|
||||
"""Add homepage wanfa category recommendations.
|
||||
|
||||
Revision ID: 0018_home_wanfa_recommendations
|
||||
Revises: 0017_home_content
|
||||
"""
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import inspect
|
||||
|
||||
|
||||
revision = "0018_home_wanfa_recommendations"
|
||||
down_revision = "0017_home_content"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
bind = op.get_bind()
|
||||
if "HomeWanfaRecommendation" in set(inspect(bind).get_table_names()):
|
||||
return
|
||||
|
||||
op.create_table(
|
||||
"HomeWanfaRecommendation",
|
||||
sa.Column("id", sa.String(), nullable=False),
|
||||
sa.Column("categoryId", 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.ForeignKeyConstraint(["categoryId"], ["WanfaCategory.id"], ondelete="RESTRICT"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("categoryId", name="uq_HomeWanfaRecommendation_categoryId"),
|
||||
)
|
||||
op.create_index("ix_HomeWanfaRecommendation_categoryId", "HomeWanfaRecommendation", ["categoryId"])
|
||||
op.create_index("ix_HomeWanfaRecommendation_isActive", "HomeWanfaRecommendation", ["isActive"])
|
||||
op.create_index("ix_HomeWanfaRecommendation_sortOrder", "HomeWanfaRecommendation", ["sortOrder"])
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
bind = op.get_bind()
|
||||
if "HomeWanfaRecommendation" in set(inspect(bind).get_table_names()):
|
||||
op.drop_table("HomeWanfaRecommendation")
|
||||
@@ -0,0 +1,37 @@
|
||||
"""Add detail images to homepage wild archive cases.
|
||||
|
||||
Revision ID: 0019_home_wild_archive_images
|
||||
Revises: 0018_home_wanfa_recommendations
|
||||
"""
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
|
||||
revision = "0019_home_wild_archive_images"
|
||||
down_revision = "0018_home_wanfa_recommendations"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column(
|
||||
"HomeWildArchive",
|
||||
sa.Column(
|
||||
"images",
|
||||
postgresql.JSONB(astext_type=sa.Text()),
|
||||
nullable=False,
|
||||
server_default=sa.text("'[]'::jsonb"),
|
||||
),
|
||||
)
|
||||
op.execute(
|
||||
sa.text(
|
||||
'UPDATE "HomeWildArchive" SET "images" = jsonb_build_array("image") WHERE jsonb_array_length("images") = 0'
|
||||
)
|
||||
)
|
||||
op.alter_column("HomeWildArchive", "images", server_default=None)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column("HomeWildArchive", "images")
|
||||
@@ -0,0 +1,51 @@
|
||||
"""Add long-form detail content to homepage team buildings.
|
||||
|
||||
Revision ID: 0020_home_team_building_details
|
||||
Revises: 0019_home_wild_archive_images
|
||||
"""
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
|
||||
revision = "0020_home_team_building_details"
|
||||
down_revision = "0019_home_wild_archive_images"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column(
|
||||
"HomeTeamBuilding",
|
||||
sa.Column("detailSubtitle", sa.String(), nullable=False, server_default=""),
|
||||
)
|
||||
op.add_column(
|
||||
"HomeTeamBuilding",
|
||||
sa.Column(
|
||||
"detailParagraphs",
|
||||
postgresql.JSONB(astext_type=sa.Text()),
|
||||
nullable=False,
|
||||
server_default=sa.text("'[]'::jsonb"),
|
||||
),
|
||||
)
|
||||
op.execute(
|
||||
sa.text(
|
||||
'UPDATE "HomeTeamBuilding" '
|
||||
'SET "detailSubtitle" = CASE '
|
||||
'WHEN "detailSubtitle" = \'\' THEN "description" '
|
||||
'ELSE "detailSubtitle" END, '
|
||||
'"detailParagraphs" = CASE '
|
||||
'WHEN jsonb_array_length("detailParagraphs") = 0 '
|
||||
'THEN jsonb_build_array("description") '
|
||||
'ELSE "detailParagraphs" END '
|
||||
'WHERE "detailSubtitle" = \'\' OR jsonb_array_length("detailParagraphs") = 0'
|
||||
)
|
||||
)
|
||||
op.alter_column("HomeTeamBuilding", "detailSubtitle", server_default=None)
|
||||
op.alter_column("HomeTeamBuilding", "detailParagraphs", server_default=None)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column("HomeTeamBuilding", "detailParagraphs")
|
||||
op.drop_column("HomeTeamBuilding", "detailSubtitle")
|
||||
Reference in new issue
Block a user