完成首页数据模型的统一重构,具体变更如下: 1. 重构公共首页API接口,移除`playRecommendations`字段,将玩法推荐数据统一放入`experiences`字段 2. 更新前后端类型定义、序列化逻辑与前端页面组件,适配新的API响应结构 3. 为微信登录相关接口添加`trust_env=True`配置,支持企业代理环境并新增`socksio`依赖 4. 新增图片画廊上传组件,重构SingleImageUploader组件支持自定义宽高比 5. 重构Toast与Select组件的实现与样式,统一后台UI设计系统 6. 移除个人中心页面不必要的返回事件与顶部标题组件,优化详情卡片布局 7. 新增微信接口单元测试,更新官方文档与测试用例适配变更 8. 删除过期的文档图片资源
560 lines
19 KiB
Python
560 lines
19 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,
|
|
HomeExperience,
|
|
HomeTeamBuilding,
|
|
HomeWanfaRecommendation,
|
|
HomeWildArchive,
|
|
WanfaCategory,
|
|
WanfaRoute,
|
|
)
|
|
from app.schemas import HomeExperienceCreate, 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_experience(**overrides):
|
|
return HomeExperience(
|
|
id=overrides.get("id", "waterfall-descent"),
|
|
badge=overrides.get("badge", "玩过推荐"),
|
|
category=overrides.get("category", "瀑降体验"),
|
|
title=overrides.get("title", "悬崖瀑降"),
|
|
englishTitle=overrides.get("englishTitle", "WATERFALL DESCENT"),
|
|
image=overrides.get("image", "https://example.test/waterfall.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_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_experience_schema_normalizes_text_and_url():
|
|
item = HomeExperienceCreate(
|
|
badge=" 玩过推荐 ",
|
|
category=" 瀑降体验 ",
|
|
title=" 悬崖瀑降 ",
|
|
englishTitle=" WATERFALL DESCENT ",
|
|
image=" https://example.test/waterfall.jpg ",
|
|
demandKeyword=" 悬崖瀑降 ",
|
|
)
|
|
|
|
assert item.title == "悬崖瀑降"
|
|
assert item.image == "https://example.test/waterfall.jpg"
|
|
assert item.sortOrder is None
|
|
|
|
|
|
def test_home_experience_schema_rejects_invalid_image_url():
|
|
with pytest.raises(ValidationError):
|
|
HomeExperienceCreate(
|
|
badge="玩过推荐",
|
|
category="瀑降体验",
|
|
title="悬崖瀑降",
|
|
englishTitle="WATERFALL DESCENT",
|
|
image="/waterfall.jpg",
|
|
demandKeyword="悬崖瀑降",
|
|
)
|
|
|
|
|
|
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_experience_list_returns_full_record():
|
|
fake_db = FakeDb(items=[make_experience()])
|
|
app = admin_app(fake_db)
|
|
try:
|
|
response = TestClient(app).get("/api/admin/home/experiences")
|
|
finally:
|
|
app.dependency_overrides.clear()
|
|
|
|
assert response.status_code == 200
|
|
item = response.json()["data"]["items"][0]
|
|
assert item["id"] == "waterfall-descent"
|
|
assert item["isActive"] is True
|
|
assert item["createdAt"] == "2026-01-01T00:00:00"
|
|
|
|
|
|
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_admin_home_delete_normalizes_remaining_order():
|
|
first = make_experience(id="experience-1", sortOrder=0)
|
|
second = make_experience(id="experience-2", sortOrder=1)
|
|
fake_db = FakeDb(items=[first, second])
|
|
app = admin_app(fake_db)
|
|
try:
|
|
response = TestClient(app).delete("/api/admin/home/experiences/experience-1")
|
|
finally:
|
|
app.dependency_overrides.clear()
|
|
|
|
assert response.status_code == 200
|
|
assert response.json() == {
|
|
"code": 200,
|
|
"msg": "success",
|
|
"data": {"id": "experience-1"},
|
|
}
|
|
assert second.sortOrder == 0
|
|
assert fake_db.committed
|
|
|
|
|
|
def test_admin_home_reorder_rejects_incomplete_ids():
|
|
fake_db = FakeDb(items=[make_experience(id="experience-1"), make_experience(id="experience-2", sortOrder=1)])
|
|
app = admin_app(fake_db)
|
|
try:
|
|
response = TestClient(app).patch(
|
|
"/api/admin/home/experiences/reorder",
|
|
json={"itemIds": ["experience-1"]},
|
|
)
|
|
finally:
|
|
app.dependency_overrides.clear()
|
|
|
|
assert response.status_code == 400
|
|
assert response.json()["errorCode"] == "HOME_REORDER_INVALID"
|
|
assert not fake_db.committed
|
|
|
|
|
|
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={
|
|
HomeExperience: [],
|
|
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={
|
|
HomeExperience: [make_experience()],
|
|
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]
|