- 移除所有废弃的旧首页配置模块,包括destinationHero、demand相关、HomeExperience、vehicleService等表结构与代码,新增数据库迁移删除遗留表 - 重构管理端后台布局为标准RuoYi风格,新增Sidebar、Navbar、SettingsDrawer等组件,替换旧的过渡菜单实现 - 调整移动端响应断点为768px,更新布局相关测试用例,新增布局组件测试 - 删除OperationsTools发布/重置页面,移除/tools菜单入口,清理对应的API调用与测试代码 - 重构玩法模块编辑器:替换旧的图片手动输入框为图片上传组件,移除冗余的取消按钮 - 新增线索查询offset参数支持,完善查询参数处理逻辑 - 优化rbac菜单构建逻辑,新增可见性控制参数 - 修正管家编辑器提示文案,优化系统资源编辑器表单初始化逻辑 - 清理小程序端废弃的体验推荐组件与相关测试代码 - 更新文档与种子脚本,匹配新的API契约与使用说明
463 lines
15 KiB
Python
463 lines
15 KiB
Python
from datetime import datetime
|
|
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
from pydantic import ValidationError
|
|
|
|
from app.auth import require_admin
|
|
from app.database import get_db
|
|
from app.main import create_app
|
|
from app.models import (
|
|
AdminUser,
|
|
HomeTeamBuilding,
|
|
HomeWanfaRecommendation,
|
|
HomeWildArchive,
|
|
WanfaCategory,
|
|
WanfaRoute,
|
|
)
|
|
from app.schemas import HomeTeamBuildingCreate, HomeWanfaRecommendationCreate, HomeWildArchiveCreate
|
|
|
|
|
|
class FakeScalarResult:
|
|
def __init__(self, items):
|
|
self.items = items
|
|
|
|
def all(self):
|
|
return self.items
|
|
|
|
|
|
class FakeDb:
|
|
def __init__(self, *, items=None, items_by_model=None, scalar_values=None):
|
|
self.items = list(items or [])
|
|
self.items_by_model = items_by_model or {}
|
|
self.scalar_values = list(scalar_values or [])
|
|
self.added = []
|
|
self.deleted = []
|
|
self.committed = False
|
|
|
|
def scalars(self, statement):
|
|
entity = statement.column_descriptions[0].get("entity")
|
|
return FakeScalarResult(self.items_by_model.get(entity, self.items))
|
|
|
|
def scalar(self, _statement):
|
|
return self.scalar_values.pop(0) if self.scalar_values else None
|
|
|
|
def get(self, model, item_id):
|
|
candidates = self.items_by_model.get(model, self.items)
|
|
return next((item for item in candidates if isinstance(item, model) and item.id == item_id), None)
|
|
|
|
def add(self, item):
|
|
self.added.append(item)
|
|
self.items.append(item)
|
|
|
|
def delete(self, item):
|
|
self.deleted.append(item)
|
|
if item in self.items:
|
|
self.items.remove(item)
|
|
|
|
def flush(self):
|
|
for item in self.added:
|
|
if not getattr(item, "id", None):
|
|
item.id = "home-item-test-id"
|
|
|
|
def commit(self):
|
|
self.committed = True
|
|
|
|
|
|
def make_admin_user():
|
|
return AdminUser(
|
|
id="admin-test",
|
|
email="admin@example.test",
|
|
name="Admin",
|
|
role="admin",
|
|
passwordHash="not-used",
|
|
isActive=True,
|
|
)
|
|
|
|
|
|
def make_team(**overrides):
|
|
return HomeTeamBuilding(
|
|
id=overrides.get("id", "wild-challenge"),
|
|
tag=overrides.get("tag", "户外挑战"),
|
|
title=overrides.get("title", "山野挑战,共创极境"),
|
|
description=overrides.get("description", "洞穴、瀑降与协作,适合 10-30 人。"),
|
|
image=overrides.get("image", "https://example.test/team.jpg"),
|
|
demandKeyword=overrides.get("demandKeyword", "户外团建"),
|
|
detailSubtitle=overrides.get("detailSubtitle", "越过山丘,向来处去"),
|
|
detailParagraphs=overrides.get("detailParagraphs", ["真正的贵州,从未被写进攻略里的山野故事。", "我们用一次次出发,把团队重新带回自然。"]),
|
|
isActive=overrides.get("isActive", True),
|
|
sortOrder=overrides.get("sortOrder", 0),
|
|
createdAt=datetime(2026, 1, 1),
|
|
updatedAt=datetime(2026, 1, 2),
|
|
)
|
|
|
|
|
|
def make_archive(**overrides):
|
|
return HomeWildArchive(
|
|
id=overrides.get("id", "hundred-meter-descent"),
|
|
title=overrides.get("title", "百米自降"),
|
|
image=overrides.get("image", "https://example.test/archive.jpg"),
|
|
images=overrides.get("images", ["https://example.test/archive.jpg", "https://example.test/archive-2.jpg"]),
|
|
demandKeyword=overrides.get("demandKeyword", "悬崖瀑降"),
|
|
isActive=overrides.get("isActive", True),
|
|
sortOrder=overrides.get("sortOrder", 0),
|
|
createdAt=datetime(2026, 1, 1),
|
|
updatedAt=datetime(2026, 1, 2),
|
|
)
|
|
|
|
|
|
def make_wanfa_category(**overrides):
|
|
route = WanfaRoute(
|
|
id="family-water",
|
|
categoryId=overrides.get("id", "family-route"),
|
|
title="亲子玩水",
|
|
subtitle="贵州·轻松节奏与自然课堂",
|
|
image="https://example.test/family-water.jpg",
|
|
routeCount=4,
|
|
demandKeyword="亲子玩水",
|
|
sortOrder=0,
|
|
)
|
|
return WanfaCategory(
|
|
id=overrides.get("id", "family-route"),
|
|
label=overrides.get("label", "亲子路线"),
|
|
sortOrder=0,
|
|
routes=[route],
|
|
)
|
|
|
|
|
|
def make_wanfa_recommendation(category=None, **overrides):
|
|
category = category or make_wanfa_category()
|
|
return HomeWanfaRecommendation(
|
|
id=overrides.get("id", "recommendation-1"),
|
|
categoryId=category.id,
|
|
category=category,
|
|
isActive=overrides.get("isActive", True),
|
|
sortOrder=overrides.get("sortOrder", 0),
|
|
createdAt=datetime(2026, 1, 1),
|
|
updatedAt=datetime(2026, 1, 2),
|
|
)
|
|
|
|
|
|
def admin_app(fake_db):
|
|
app = create_app()
|
|
app.dependency_overrides[get_db] = lambda: fake_db
|
|
app.dependency_overrides[require_admin] = make_admin_user
|
|
return app
|
|
|
|
|
|
def test_home_wild_archive_schema_normalizes_detail_image_urls():
|
|
item = HomeWildArchiveCreate(
|
|
title=" 石龙洞 ",
|
|
image=" https://example.test/cover.jpg ",
|
|
images=[" https://example.test/one.jpg ", "https://example.test/two.jpg"],
|
|
demandKeyword=" 地心探险 ",
|
|
)
|
|
|
|
assert item.title == "石龙洞"
|
|
assert item.image == "https://example.test/cover.jpg"
|
|
assert item.images == ["https://example.test/one.jpg", "https://example.test/two.jpg"]
|
|
|
|
|
|
def test_home_team_building_schema_normalizes_detail_content():
|
|
item = HomeTeamBuildingCreate(
|
|
tag=" 户外挑战 ",
|
|
title=" 山野挑战,共创极境 ",
|
|
description=" 洞穴、瀑降与协作。 ",
|
|
image=" https://example.test/team.jpg ",
|
|
demandKeyword=" 户外团建 ",
|
|
detailSubtitle=" 越过山丘,向来处去 ",
|
|
detailParagraphs=[" 第一段 ", "", " 第二段 "],
|
|
)
|
|
|
|
assert item.detailSubtitle == "越过山丘,向来处去"
|
|
assert item.detailParagraphs == ["第一段", "第二段"]
|
|
|
|
|
|
def test_admin_home_team_create_appends_to_end():
|
|
fake_db = FakeDb(scalar_values=[2])
|
|
app = admin_app(fake_db)
|
|
try:
|
|
response = TestClient(app).post(
|
|
"/api/admin/home/team-buildings",
|
|
json={
|
|
"tag": " 团队协作 ",
|
|
"title": "峡谷溯溪,默契同行",
|
|
"description": "溯溪与分组协作,兼顾参与感与安全。",
|
|
"image": "https://example.test/team.jpg",
|
|
"demandKeyword": "峡谷团建",
|
|
"detailSubtitle": "并肩探索,共创默契",
|
|
"detailParagraphs": ["第一段正文", "第二段正文"],
|
|
},
|
|
)
|
|
finally:
|
|
app.dependency_overrides.clear()
|
|
|
|
assert response.status_code == 201
|
|
assert fake_db.added[0].tag == "团队协作"
|
|
assert fake_db.added[0].detailSubtitle == "并肩探索,共创默契"
|
|
assert fake_db.added[0].detailParagraphs == ["第一段正文", "第二段正文"]
|
|
assert fake_db.added[0].sortOrder == 3
|
|
assert fake_db.committed
|
|
|
|
|
|
def test_admin_home_wild_patch_updates_record():
|
|
archive = make_archive()
|
|
fake_db = FakeDb(items=[archive])
|
|
app = admin_app(fake_db)
|
|
try:
|
|
response = TestClient(app).patch(
|
|
"/api/admin/home/wild-archives/hundred-meter-descent",
|
|
json={"title": "百米自降体验", "isActive": False},
|
|
)
|
|
finally:
|
|
app.dependency_overrides.clear()
|
|
|
|
assert response.status_code == 200
|
|
assert response.json()["data"]["title"] == "百米自降体验"
|
|
assert response.json()["data"]["isActive"] is False
|
|
assert fake_db.committed
|
|
|
|
|
|
def test_admin_home_team_patch_updates_detail_content():
|
|
team = make_team()
|
|
fake_db = FakeDb(items=[team])
|
|
app = admin_app(fake_db)
|
|
try:
|
|
response = TestClient(app).patch(
|
|
"/api/admin/home/team-buildings/wild-challenge",
|
|
json={
|
|
"detailSubtitle": " 并肩探索,共创默契 ",
|
|
"detailParagraphs": [" 第一段正文 ", "", " 第二段正文 "],
|
|
},
|
|
)
|
|
finally:
|
|
app.dependency_overrides.clear()
|
|
|
|
assert response.status_code == 200
|
|
assert team.detailSubtitle == "并肩探索,共创默契"
|
|
assert team.detailParagraphs == ["第一段正文", "第二段正文"]
|
|
assert fake_db.committed
|
|
|
|
|
|
def test_admin_home_wild_create_persists_detail_images():
|
|
fake_db = FakeDb(scalar_values=[0])
|
|
app = admin_app(fake_db)
|
|
try:
|
|
response = TestClient(app).post(
|
|
"/api/admin/home/wild-archives",
|
|
json={
|
|
"title": "石龙洞——客片案例",
|
|
"image": "https://example.test/cover.jpg",
|
|
"images": ["https://example.test/cover.jpg", "https://example.test/inside.jpg"],
|
|
"demandKeyword": "地心探险",
|
|
},
|
|
)
|
|
finally:
|
|
app.dependency_overrides.clear()
|
|
|
|
assert response.status_code == 201
|
|
assert fake_db.added[0].images == [
|
|
"https://example.test/cover.jpg",
|
|
"https://example.test/inside.jpg",
|
|
]
|
|
|
|
|
|
def test_home_wanfa_recommendation_schema_requires_category_id():
|
|
item = HomeWanfaRecommendationCreate(categoryId=" family-route ")
|
|
|
|
assert item.categoryId == "family-route"
|
|
|
|
|
|
def test_admin_home_wanfa_recommendation_create_links_existing_category():
|
|
category = make_wanfa_category()
|
|
fake_db = FakeDb(items=[category], scalar_values=[0])
|
|
app = admin_app(fake_db)
|
|
try:
|
|
response = TestClient(app).post(
|
|
"/api/admin/home/play-recommendations",
|
|
json={"categoryId": " family-route "},
|
|
)
|
|
finally:
|
|
app.dependency_overrides.clear()
|
|
|
|
assert response.status_code == 201
|
|
assert response.json()["data"]["categoryId"] == "family-route"
|
|
assert response.json()["data"]["categoryLabel"] == "亲子路线"
|
|
assert fake_db.added[0].categoryId == "family-route"
|
|
|
|
|
|
def test_public_home_places_active_wanfa_category_recommendations_in_experiences():
|
|
recommendation = make_wanfa_recommendation()
|
|
app = create_app()
|
|
fake_db = FakeDb(
|
|
items_by_model={
|
|
HomeTeamBuilding: [],
|
|
HomeWildArchive: [],
|
|
HomeWanfaRecommendation: [recommendation],
|
|
}
|
|
)
|
|
app.dependency_overrides[get_db] = lambda: fake_db
|
|
try:
|
|
response = TestClient(app).get("/api/public/home")
|
|
finally:
|
|
app.dependency_overrides.clear()
|
|
|
|
assert response.status_code == 200
|
|
payload = response.json()["data"]
|
|
assert payload["experiences"] == [
|
|
{
|
|
"id": "recommendation-1",
|
|
"categoryId": "family-route",
|
|
"label": "亲子路线",
|
|
"routes": [
|
|
{
|
|
"id": "family-water",
|
|
"title": "亲子玩水",
|
|
"subtitle": "贵州·轻松节奏与自然课堂",
|
|
"image": "https://example.test/family-water.jpg",
|
|
"routeCount": 4,
|
|
"demandKeyword": "亲子玩水",
|
|
}
|
|
],
|
|
}
|
|
]
|
|
assert "playRecommendations" not in payload
|
|
|
|
|
|
def test_public_home_returns_rendering_fields_for_all_sections():
|
|
app = create_app()
|
|
fake_db = FakeDb(
|
|
items_by_model={
|
|
HomeTeamBuilding: [make_team()],
|
|
HomeWildArchive: [make_archive()],
|
|
}
|
|
)
|
|
app.dependency_overrides[get_db] = lambda: fake_db
|
|
try:
|
|
response = TestClient(app).get("/api/public/home")
|
|
finally:
|
|
app.dependency_overrides.clear()
|
|
|
|
assert response.status_code == 200
|
|
assert response.json() == {
|
|
"code": 200,
|
|
"msg": "success",
|
|
"data": {
|
|
"experiences": [],
|
|
"teamBuildings": [
|
|
{
|
|
"id": "wild-challenge",
|
|
"tag": "户外挑战",
|
|
"title": "山野挑战,共创极境",
|
|
"description": "洞穴、瀑降与协作,适合 10-30 人。",
|
|
"image": "https://example.test/team.jpg",
|
|
"demandKeyword": "户外团建",
|
|
}
|
|
],
|
|
"wildArchives": [
|
|
{
|
|
"id": "hundred-meter-descent",
|
|
"title": "百米自降",
|
|
"image": "https://example.test/archive.jpg",
|
|
"demandKeyword": "悬崖瀑降",
|
|
"photoCount": 2,
|
|
}
|
|
],
|
|
},
|
|
}
|
|
|
|
|
|
def test_public_wild_archive_list_returns_photo_count():
|
|
archive = make_archive()
|
|
app = create_app()
|
|
fake_db = FakeDb(items_by_model={HomeWildArchive: [archive]})
|
|
app.dependency_overrides[get_db] = lambda: fake_db
|
|
try:
|
|
response = TestClient(app).get("/api/public/home/wild-archives")
|
|
finally:
|
|
app.dependency_overrides.clear()
|
|
|
|
assert response.status_code == 200
|
|
assert response.json()["data"]["items"][0] == {
|
|
"id": "hundred-meter-descent",
|
|
"title": "百米自降",
|
|
"image": "https://example.test/archive.jpg",
|
|
"demandKeyword": "悬崖瀑降",
|
|
"photoCount": 2,
|
|
}
|
|
|
|
|
|
def test_public_wild_archive_detail_returns_all_images():
|
|
archive = make_archive()
|
|
app = create_app()
|
|
fake_db = FakeDb(scalar_values=[archive])
|
|
app.dependency_overrides[get_db] = lambda: fake_db
|
|
try:
|
|
response = TestClient(app).get("/api/public/home/wild-archives/hundred-meter-descent")
|
|
finally:
|
|
app.dependency_overrides.clear()
|
|
|
|
assert response.status_code == 200
|
|
assert response.json()["data"]["title"] == "百米自降"
|
|
assert response.json()["data"]["images"] == [
|
|
"https://example.test/archive.jpg",
|
|
"https://example.test/archive-2.jpg",
|
|
]
|
|
|
|
|
|
def test_public_team_building_detail_returns_long_form_content():
|
|
team = make_team()
|
|
app = create_app()
|
|
fake_db = FakeDb(scalar_values=[team])
|
|
app.dependency_overrides[get_db] = lambda: fake_db
|
|
try:
|
|
response = TestClient(app).get("/api/public/home/team-buildings/wild-challenge")
|
|
finally:
|
|
app.dependency_overrides.clear()
|
|
|
|
assert response.status_code == 200
|
|
assert response.json() == {
|
|
"code": 200,
|
|
"msg": "success",
|
|
"data": {
|
|
"id": "wild-challenge",
|
|
"tag": "户外挑战",
|
|
"title": "山野挑战,共创极境",
|
|
"description": "洞穴、瀑降与协作,适合 10-30 人。",
|
|
"image": "https://example.test/team.jpg",
|
|
"demandKeyword": "户外团建",
|
|
"detailSubtitle": "越过山丘,向来处去",
|
|
"detailParagraphs": [
|
|
"真正的贵州,从未被写进攻略里的山野故事。",
|
|
"我们用一次次出发,把团队重新带回自然。",
|
|
],
|
|
},
|
|
}
|
|
|
|
|
|
def test_public_team_building_detail_returns_not_found_for_missing_item():
|
|
app = create_app()
|
|
app.dependency_overrides[get_db] = lambda: FakeDb(scalar_values=[None])
|
|
try:
|
|
response = TestClient(app).get("/api/public/home/team-buildings/missing")
|
|
finally:
|
|
app.dependency_overrides.clear()
|
|
|
|
assert response.status_code == 404
|
|
assert response.json()["msg"] == "团队共创不存在"
|
|
|
|
|
|
def test_public_team_building_detail_falls_back_to_legacy_description():
|
|
team = make_team(detailSubtitle="", detailParagraphs=[])
|
|
app = create_app()
|
|
fake_db = FakeDb(scalar_values=[team])
|
|
app.dependency_overrides[get_db] = lambda: fake_db
|
|
try:
|
|
response = TestClient(app).get("/api/public/home/team-buildings/wild-challenge")
|
|
finally:
|
|
app.dependency_overrides.clear()
|
|
|
|
assert response.status_code == 200
|
|
assert response.json()["data"]["detailSubtitle"] == team.description
|
|
assert response.json()["data"]["detailParagraphs"] == [team.description]
|