feat: add campaign model and API integration with price and tags
This commit is contained in:
@@ -8,9 +8,10 @@ from app import serializers
|
||||
from app.auth import hash_password, require_admin
|
||||
from app.database import get_db
|
||||
from app.main import create_app
|
||||
from app.models import AdminUser, CtaBanner, Destination, DestinationAlias, HeroSlide, Lead, MapImage, MediaAsset, Product, ProductImage, ThemeCard
|
||||
from app.models import AdminUser, Campaign, CtaBanner, Destination, DestinationAlias, HeroSlide, Lead, MapImage, MediaAsset, Product, ProductImage, ThemeCard
|
||||
from app.routers import admin as admin_router
|
||||
from app.routers.admin import normalize_detail_sections, normalize_images
|
||||
from app.routers.shared import site_config
|
||||
from app.schemas import (
|
||||
AdminProductQuery,
|
||||
LeadCreateIn,
|
||||
@@ -189,6 +190,24 @@ def make_map_image(**overrides):
|
||||
)
|
||||
|
||||
|
||||
def make_campaign(**overrides):
|
||||
return Campaign(
|
||||
id=overrides.get("id", "campaign-test"),
|
||||
slug=overrides.get("slug", "classic-deal"),
|
||||
title=overrides.get("title", "经典打卡特惠"),
|
||||
description=overrides.get("description", "经典首游活动"),
|
||||
coverImage=overrides.get("coverImage", "https://cdn.example.test/campaign.webp"),
|
||||
priceAmount=overrides.get("priceAmount", 162500),
|
||||
priceUnit=overrides.get("priceUnit", "起/人"),
|
||||
tags=overrides.get("tags", ["臻藏旅位", "赛事庆典"]),
|
||||
status=overrides.get("status", "draft"),
|
||||
startsAt=overrides.get("startsAt"),
|
||||
endsAt=overrides.get("endsAt"),
|
||||
createdAt=datetime(2026, 1, 1),
|
||||
updatedAt=datetime(2026, 1, 2),
|
||||
)
|
||||
|
||||
|
||||
def make_admin_user():
|
||||
return AdminUser(
|
||||
id="admin-test",
|
||||
@@ -452,7 +471,7 @@ def test_site_config_create_requires_module_primary_field():
|
||||
|
||||
def test_admin_site_config_hero_slides_use_dedicated_contract_without_targets():
|
||||
hero_slide = make_hero_slide(targetType="campaign", targetValue="campaign-test")
|
||||
fake_db = FakeDb(scalar_results=[[], [hero_slide], [], [], []])
|
||||
fake_db = FakeDb(scalar_results=[[], [hero_slide], [], [], [], []])
|
||||
app = authenticated_app(fake_db)
|
||||
|
||||
try:
|
||||
@@ -470,7 +489,7 @@ def test_admin_site_config_hero_slides_use_dedicated_contract_without_targets():
|
||||
|
||||
def test_admin_site_config_includes_map_array_with_dedicated_contract():
|
||||
map_image = make_map_image()
|
||||
fake_db = FakeDb(scalar_results=[[], [], [map_image], [], []])
|
||||
fake_db = FakeDb(scalar_results=[[], [], [map_image], [], [], []])
|
||||
app = authenticated_app(fake_db)
|
||||
|
||||
try:
|
||||
@@ -494,6 +513,40 @@ def test_admin_site_config_includes_map_array_with_dedicated_contract():
|
||||
assert "targetType" not in body["map"][0]
|
||||
|
||||
|
||||
def test_admin_site_config_includes_campaigns_for_special_offers():
|
||||
campaign = make_campaign(status="published")
|
||||
fake_db = FakeDb(scalar_results=[[], [], [], [], [], [campaign]])
|
||||
app = authenticated_app(fake_db)
|
||||
|
||||
try:
|
||||
response = TestClient(app).get("/api/admin/site-config")
|
||||
finally:
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
assert response.status_code == 200
|
||||
body = response.json()
|
||||
assert body["campaigns"] == [
|
||||
{
|
||||
"id": "campaign-test",
|
||||
"slug": "classic-deal",
|
||||
"title": "经典打卡特惠",
|
||||
"description": "经典首游活动",
|
||||
"coverImage": "https://cdn.example.test/campaign.webp",
|
||||
"priceAmount": 162500,
|
||||
"priceUnit": "起/人",
|
||||
"tags": ["臻藏旅位", "赛事庆典"],
|
||||
"status": "published",
|
||||
"startsAt": None,
|
||||
"endsAt": None,
|
||||
"createdAt": "2026-01-01T00:00:00",
|
||||
"updatedAt": "2026-01-02T00:00:00",
|
||||
}
|
||||
]
|
||||
assert "sortOrder" not in body["campaigns"][0]
|
||||
assert "isActive" not in body["campaigns"][0]
|
||||
assert "targetType" not in body["campaigns"][0]
|
||||
|
||||
|
||||
def test_site_config_create_hero_slide_ignores_target_fields_and_returns_dedicated_contract():
|
||||
fake_db = FakeDb(scalar_values=[0])
|
||||
app = authenticated_app(fake_db)
|
||||
@@ -564,6 +617,75 @@ def test_site_config_create_map_image_rejects_duplicate_singleton():
|
||||
assert not fake_db.committed
|
||||
|
||||
|
||||
def test_site_config_create_campaign_defaults_draft_and_audits():
|
||||
fake_db = FakeDb()
|
||||
app = authenticated_app(fake_db)
|
||||
|
||||
try:
|
||||
response = TestClient(app).post(
|
||||
"/api/admin/site-config/campaigns",
|
||||
json={
|
||||
"title": " Classic Deal ",
|
||||
"slug": " classic-deal ",
|
||||
"description": " Deal summary ",
|
||||
"coverImage": " https://cdn.example.test/campaign.webp ",
|
||||
"priceAmount": 188000,
|
||||
"priceUnit": " 起/人 ",
|
||||
"tags": [" 季节限定 ", "", "小众秘境"],
|
||||
"image": "ignored",
|
||||
"isActive": True,
|
||||
"sortOrder": 9,
|
||||
},
|
||||
)
|
||||
finally:
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
assert response.status_code == 201
|
||||
body = response.json()
|
||||
assert body["title"] == "Classic Deal"
|
||||
assert body["slug"] == "classic-deal"
|
||||
assert body["description"] == "Deal summary"
|
||||
assert body["coverImage"] == "https://cdn.example.test/campaign.webp"
|
||||
assert body["priceAmount"] == 188000
|
||||
assert body["priceUnit"] == "起/人"
|
||||
assert body["tags"] == ["季节限定", "小众秘境"]
|
||||
assert body["status"] == "draft"
|
||||
assert "image" not in body
|
||||
assert "isActive" not in body
|
||||
assert "sortOrder" not in body
|
||||
created = fake_db.added[0]
|
||||
assert isinstance(created, Campaign)
|
||||
assert created.status == "draft"
|
||||
assert created.priceAmount == 188000
|
||||
assert created.priceUnit == "起/人"
|
||||
assert created.tags == ["季节限定", "小众秘境"]
|
||||
assert fake_db.added[-1].entity == "campaign"
|
||||
assert fake_db.committed
|
||||
|
||||
|
||||
def test_site_config_create_campaign_rejects_more_than_three_tags():
|
||||
fake_db = FakeDb()
|
||||
app = authenticated_app(fake_db)
|
||||
|
||||
try:
|
||||
response = TestClient(app).post(
|
||||
"/api/admin/site-config/campaigns",
|
||||
json={
|
||||
"title": "Classic Deal",
|
||||
"slug": "classic-deal",
|
||||
"tags": ["a", "b", "c", "d"],
|
||||
},
|
||||
)
|
||||
finally:
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
assert response.status_code == 422
|
||||
assert response.json()["code"] == "MODULE_CONFIG_VALIDATION_ERROR"
|
||||
assert response.json()["details"] == {"field": "tags", "max": 3}
|
||||
assert not fake_db.added
|
||||
assert not fake_db.committed
|
||||
|
||||
|
||||
def test_site_config_patch_hero_slide_ignores_target_fields_and_returns_dedicated_contract():
|
||||
hero_slide = make_hero_slide(targetType="campaign", targetValue="campaign-test")
|
||||
fake_db = FakeDb(get_result=hero_slide)
|
||||
@@ -616,6 +738,81 @@ def test_site_config_patch_map_image_updates_allowed_fields_only():
|
||||
assert fake_db.added[-1].entity == "map_image"
|
||||
|
||||
|
||||
def test_site_config_patch_campaign_updates_contract_fields_only():
|
||||
campaign = make_campaign(status="draft")
|
||||
fake_db = FakeDb(get_result=campaign)
|
||||
app = authenticated_app(fake_db)
|
||||
|
||||
try:
|
||||
response = TestClient(app).patch(
|
||||
"/api/admin/site-config/campaigns/campaign-test",
|
||||
json={
|
||||
"title": " Summer Deal ",
|
||||
"slug": " summer-deal ",
|
||||
"description": None,
|
||||
"coverImage": " https://cdn.example.test/summer.webp ",
|
||||
"priceAmount": 57500,
|
||||
"priceUnit": " 起/人 ",
|
||||
"tags": ["无可比拟", "尊享礼遇", ""],
|
||||
"status": "published",
|
||||
"startsAt": "2026-07-01T08:00:00",
|
||||
"endsAt": None,
|
||||
"image": "ignored",
|
||||
"isActive": False,
|
||||
"sortOrder": 3,
|
||||
},
|
||||
)
|
||||
finally:
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
assert response.status_code == 200
|
||||
body = response.json()
|
||||
assert body["title"] == "Summer Deal"
|
||||
assert body["slug"] == "summer-deal"
|
||||
assert body["description"] is None
|
||||
assert body["coverImage"] == "https://cdn.example.test/summer.webp"
|
||||
assert body["priceAmount"] == 57500
|
||||
assert body["priceUnit"] == "起/人"
|
||||
assert body["tags"] == ["无可比拟", "尊享礼遇"]
|
||||
assert body["status"] == "published"
|
||||
assert body["startsAt"] == "2026-07-01T08:00:00"
|
||||
assert body["endsAt"] is None
|
||||
assert "image" not in body
|
||||
assert "isActive" not in body
|
||||
assert "sortOrder" not in body
|
||||
assert campaign.status == "published"
|
||||
assert campaign.priceAmount == 57500
|
||||
assert campaign.priceUnit == "起/人"
|
||||
assert campaign.tags == ["无可比拟", "尊享礼遇"]
|
||||
assert fake_db.added[-1].action == "update"
|
||||
assert fake_db.added[-1].entity == "campaign"
|
||||
|
||||
|
||||
def test_public_site_config_campaigns_include_price_and_tags():
|
||||
campaign = make_campaign(status="published", tags=["自然奇景", "小众秘境"])
|
||||
fake_db = FakeDb(scalar_results=[[], [], [], [], [], [campaign], []])
|
||||
|
||||
result = site_config(fake_db, active_only=True)
|
||||
|
||||
assert result["campaigns"] == [
|
||||
{
|
||||
"id": "campaign-test",
|
||||
"slug": "classic-deal",
|
||||
"title": "经典打卡特惠",
|
||||
"description": "经典首游活动",
|
||||
"coverImage": "https://cdn.example.test/campaign.webp",
|
||||
"priceAmount": 162500,
|
||||
"priceUnit": "起/人",
|
||||
"tags": ["自然奇景", "小众秘境"],
|
||||
"status": "published",
|
||||
"startsAt": None,
|
||||
"endsAt": None,
|
||||
"createdAt": "2026-01-01T00:00:00",
|
||||
"updatedAt": "2026-01-02T00:00:00",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_site_config_patch_updates_destination_contract_fields():
|
||||
destination = make_destination()
|
||||
fake_db = FakeDb(get_result=destination)
|
||||
@@ -742,6 +939,20 @@ def test_site_config_reorder_map_is_not_supported():
|
||||
assert not fake_db.committed
|
||||
|
||||
|
||||
def test_site_config_reorder_campaigns_is_not_supported():
|
||||
fake_db = FakeDb()
|
||||
app = authenticated_app(fake_db)
|
||||
|
||||
try:
|
||||
response = TestClient(app).patch("/api/admin/site-config/campaigns/reorder", json={"itemIds": ["campaign-test"]})
|
||||
finally:
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
assert response.status_code == 400
|
||||
assert response.json()["code"] == "MODULE_CONFIG_REORDER_UNSUPPORTED"
|
||||
assert not fake_db.committed
|
||||
|
||||
|
||||
def test_site_config_delete_map_image_returns_json_and_audits_without_reorder():
|
||||
map_image = make_map_image()
|
||||
fake_db = FakeDb(get_result=map_image)
|
||||
@@ -760,6 +971,24 @@ def test_site_config_delete_map_image_returns_json_and_audits_without_reorder():
|
||||
assert fake_db.committed
|
||||
|
||||
|
||||
def test_site_config_delete_campaign_returns_json_and_audits_without_reorder():
|
||||
campaign = make_campaign()
|
||||
fake_db = FakeDb(get_result=campaign)
|
||||
app = authenticated_app(fake_db)
|
||||
|
||||
try:
|
||||
response = TestClient(app).delete("/api/admin/site-config/campaigns/campaign-test")
|
||||
finally:
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"id": "campaign-test"}
|
||||
assert fake_db.deleted == [campaign]
|
||||
assert fake_db.added[-1].action == "delete"
|
||||
assert fake_db.added[-1].entity == "campaign"
|
||||
assert fake_db.committed
|
||||
|
||||
|
||||
def test_admin_media_upload_streams_image_to_oss_records_asset_and_audits(monkeypatch):
|
||||
uploaded = {}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user