feat: add customer travel records and browse history
This commit is contained in:
1 parent
0139fe6382
commit
6d9a546918
79 files changed
+12362
-112
No files matched your search
@@ -0,0 +1,92 @@
|
||||
"""Create the customer browse history table for public record pages."""
|
||||
|
||||
from alembic import context, op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import inspect
|
||||
|
||||
from app.migration_compat import baseline_table_names
|
||||
|
||||
|
||||
revision = "0036_customer_browse_history"
|
||||
down_revision = "0035_concierge_details_jsonb"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
TABLE_NAME = "CustomerBrowseHistory"
|
||||
CUSTOMER_HISTORY_INDEXES = {
|
||||
"ix_CustomerBrowseHistory_customerId",
|
||||
"ix_CustomerBrowseHistory_customer_visitedAt",
|
||||
}
|
||||
|
||||
|
||||
def is_offline_mode() -> bool:
|
||||
try:
|
||||
return context.is_offline_mode()
|
||||
except (NameError, AttributeError):
|
||||
return False
|
||||
|
||||
|
||||
def table_names() -> set[str]:
|
||||
if is_offline_mode():
|
||||
return baseline_table_names()
|
||||
return set(inspect(op.get_bind()).get_table_names())
|
||||
|
||||
|
||||
def should_create_table(existing_tables: set[str]) -> bool:
|
||||
return TABLE_NAME not in existing_tables
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
if not should_create_table(table_names()):
|
||||
return
|
||||
|
||||
op.create_table(
|
||||
TABLE_NAME,
|
||||
sa.Column("id", sa.String(), nullable=False),
|
||||
sa.Column("customerId", sa.String(), nullable=False),
|
||||
sa.Column("itemType", sa.String(), nullable=False),
|
||||
sa.Column("itemId", sa.String(), nullable=False),
|
||||
sa.Column("title", sa.String(), nullable=False),
|
||||
sa.Column("image", sa.String(), nullable=False),
|
||||
sa.Column("visitedAt", sa.DateTime(), nullable=False),
|
||||
sa.Column("createdAt", sa.DateTime(), nullable=False),
|
||||
sa.Column("updatedAt", sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
["customerId"],
|
||||
["Customer.id"],
|
||||
name="fk_CustomerBrowseHistory_customerId_Customer",
|
||||
ondelete="CASCADE",
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint(
|
||||
"customerId",
|
||||
"itemType",
|
||||
"itemId",
|
||||
name="uq_CustomerBrowseHistory_customer_item",
|
||||
),
|
||||
)
|
||||
op.create_index(
|
||||
"ix_CustomerBrowseHistory_customerId",
|
||||
TABLE_NAME,
|
||||
["customerId"],
|
||||
)
|
||||
op.create_index(
|
||||
"ix_CustomerBrowseHistory_customer_visitedAt",
|
||||
TABLE_NAME,
|
||||
["customerId", "visitedAt"],
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
if should_create_table(table_names()):
|
||||
return
|
||||
|
||||
op.drop_index(
|
||||
"ix_CustomerBrowseHistory_customer_visitedAt",
|
||||
table_name=TABLE_NAME,
|
||||
)
|
||||
op.drop_index(
|
||||
"ix_CustomerBrowseHistory_customerId",
|
||||
table_name=TABLE_NAME,
|
||||
)
|
||||
op.drop_table(TABLE_NAME)
|
||||
@@ -1,6 +1,6 @@
|
||||
from datetime import datetime, timezone
|
||||
from uuid import uuid4
|
||||
from sqlalchemy import Boolean, DateTime, ForeignKey, Integer, String, Text, UniqueConstraint
|
||||
from sqlalchemy import Boolean, DateTime, ForeignKey, Index, Integer, String, Text, UniqueConstraint
|
||||
from sqlalchemy.dialects.postgresql import JSONB
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from .database import Base
|
||||
@@ -344,6 +344,44 @@ class Customer(Base):
|
||||
updatedAt: Mapped[datetime] = mapped_column(DateTime, default=utc_now, onupdate=utc_now, nullable=False)
|
||||
|
||||
orders: Mapped[list["Order"]] = relationship(back_populates="customer")
|
||||
browseHistory: Mapped[list["CustomerBrowseHistory"]] = relationship(
|
||||
back_populates="customer",
|
||||
cascade="all, delete-orphan",
|
||||
)
|
||||
|
||||
|
||||
class CustomerBrowseHistory(Base):
|
||||
__tablename__ = "CustomerBrowseHistory"
|
||||
__table_args__ = (
|
||||
UniqueConstraint(
|
||||
"customerId",
|
||||
"itemType",
|
||||
"itemId",
|
||||
name="uq_CustomerBrowseHistory_customer_item",
|
||||
),
|
||||
Index(
|
||||
"ix_CustomerBrowseHistory_customer_visitedAt",
|
||||
"customerId",
|
||||
"visitedAt",
|
||||
),
|
||||
)
|
||||
|
||||
id: Mapped[str] = mapped_column(String, primary_key=True, default=new_id)
|
||||
customerId: Mapped[str] = mapped_column(
|
||||
String,
|
||||
ForeignKey("Customer.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
index=True,
|
||||
)
|
||||
itemType: Mapped[str] = mapped_column(String, nullable=False)
|
||||
itemId: Mapped[str] = mapped_column(String, nullable=False)
|
||||
title: Mapped[str] = mapped_column(String, nullable=False)
|
||||
image: Mapped[str] = mapped_column(String, nullable=False, default="")
|
||||
visitedAt: Mapped[datetime] = mapped_column(DateTime, default=utc_now, nullable=False)
|
||||
createdAt: Mapped[datetime] = mapped_column(DateTime, default=utc_now, nullable=False)
|
||||
updatedAt: Mapped[datetime] = mapped_column(DateTime, default=utc_now, onupdate=utc_now, nullable=False)
|
||||
|
||||
customer: Mapped[Customer] = relationship(back_populates="browseHistory")
|
||||
|
||||
|
||||
class Order(Base):
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
from datetime import datetime, time
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy import select
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.orm import Session, selectinload
|
||||
from .shared import site_config
|
||||
from ..auth import create_customer_token, optional_customer, require_customer
|
||||
from ..api_response import success_response
|
||||
from ..database import get_db
|
||||
from ..models import ConciergeAdvisor, Customer, DetailRecord, HomeTeamBuilding, HomeWanfaRecommendation, HomeWildArchive, Lead, WanfaCategory
|
||||
from ..schemas import LeadCreateIn, PhoneLoginIn
|
||||
from ..models import ConciergeAdvisor, Customer, CustomerBrowseHistory, DetailRecord, HomeTeamBuilding, HomeWanfaRecommendation, HomeWildArchive, Lead, WanfaCategory, WanfaRoute, utc_now
|
||||
from ..schemas import BrowseHistoryCreateIn, LeadCreateIn, PhoneLoginIn
|
||||
from ..media_urls import resolve_media_url
|
||||
from ..serializers import (
|
||||
public_concierge_advisor_dict,
|
||||
public_home_team_building_dict,
|
||||
@@ -35,6 +36,52 @@ def public_customer_dict(customer: Customer) -> dict:
|
||||
return {"id": customer.id, "phoneMasked": mask_phone(customer.phone)}
|
||||
|
||||
|
||||
def public_vehicle_demand_dict(lead: Lead) -> dict:
|
||||
return {
|
||||
"id": lead.id,
|
||||
"status": lead.status,
|
||||
"contactName": lead.contactName,
|
||||
"phoneMasked": mask_phone(lead.phone),
|
||||
"destination": lead.destination,
|
||||
"travelDate": lead.travelDate.isoformat() if lead.travelDate else None,
|
||||
"peopleCount": lead.peopleCount,
|
||||
"note": lead.note,
|
||||
"vehicleDemand": lead.vehicleDemand or {},
|
||||
"createdAt": lead.createdAt.isoformat(),
|
||||
"updatedAt": lead.updatedAt.isoformat(),
|
||||
}
|
||||
|
||||
|
||||
def public_browse_history_dict(item: CustomerBrowseHistory) -> dict:
|
||||
return {
|
||||
"id": item.id,
|
||||
"itemType": item.itemType,
|
||||
"itemId": item.itemId,
|
||||
"title": item.title,
|
||||
"image": resolve_media_url(item.image) or "",
|
||||
"visitedAt": item.visitedAt.isoformat(),
|
||||
}
|
||||
|
||||
|
||||
def browse_source(item_type: str, item_id: str, db: Session):
|
||||
models = {
|
||||
"wanfa-route": WanfaRoute,
|
||||
"team-building": HomeTeamBuilding,
|
||||
"wild-archive": HomeWildArchive,
|
||||
}
|
||||
model = models.get(item_type)
|
||||
if model is None:
|
||||
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="浏览内容类型不支持")
|
||||
|
||||
conditions = [model.id == item_id]
|
||||
if hasattr(model, "isActive"):
|
||||
conditions.append(model.isActive.is_(True))
|
||||
source = db.scalar(select(model).where(*conditions))
|
||||
if not source:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="浏览内容不存在")
|
||||
return source
|
||||
|
||||
|
||||
@router.get("/health")
|
||||
def health():
|
||||
return success_response({"ok": True, "service": "miniapp-api"})
|
||||
@@ -170,6 +217,113 @@ def current_customer(customer: Customer = Depends(require_customer)):
|
||||
return success_response(public_customer_dict(customer))
|
||||
|
||||
|
||||
@router.get("/api/public/customer/vehicle-demands")
|
||||
def list_customer_vehicle_demands(
|
||||
pageNum: int = Query(default=1, ge=1),
|
||||
pageSize: int = Query(default=20, ge=1, le=50),
|
||||
customer: Customer = Depends(require_customer),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
filters = [Lead.customerId == customer.id, Lead.leadType == "vehicle"]
|
||||
leads = db.scalars(
|
||||
select(Lead)
|
||||
.where(*filters)
|
||||
.order_by(Lead.createdAt.desc())
|
||||
.offset((pageNum - 1) * pageSize)
|
||||
.limit(pageSize)
|
||||
).all()
|
||||
total = db.scalar(select(func.count()).select_from(Lead).where(*filters)) or 0
|
||||
return success_response(
|
||||
{
|
||||
"items": [public_vehicle_demand_dict(lead) for lead in leads],
|
||||
"total": total,
|
||||
"pageNum": pageNum,
|
||||
"pageSize": pageSize,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@router.get("/api/public/customer/vehicle-demands/{lead_id}")
|
||||
def get_customer_vehicle_demand(
|
||||
lead_id: str,
|
||||
customer: Customer = Depends(require_customer),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
lead = db.scalar(
|
||||
select(Lead).where(
|
||||
Lead.id == lead_id,
|
||||
Lead.customerId == customer.id,
|
||||
Lead.leadType == "vehicle",
|
||||
)
|
||||
)
|
||||
if not lead:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="用车记录不存在")
|
||||
return success_response(public_vehicle_demand_dict(lead))
|
||||
|
||||
|
||||
@router.get("/api/public/customer/browse-history")
|
||||
def list_customer_browse_history(
|
||||
pageNum: int = Query(default=1, ge=1),
|
||||
pageSize: int = Query(default=20, ge=1, le=50),
|
||||
customer: Customer = Depends(require_customer),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
filters = [CustomerBrowseHistory.customerId == customer.id]
|
||||
items = db.scalars(
|
||||
select(CustomerBrowseHistory)
|
||||
.where(*filters)
|
||||
.order_by(CustomerBrowseHistory.visitedAt.desc())
|
||||
.offset((pageNum - 1) * pageSize)
|
||||
.limit(pageSize)
|
||||
).all()
|
||||
total = db.scalar(select(func.count()).select_from(CustomerBrowseHistory).where(*filters)) or 0
|
||||
return success_response(
|
||||
{
|
||||
"items": [public_browse_history_dict(item) for item in items],
|
||||
"total": total,
|
||||
"pageNum": pageNum,
|
||||
"pageSize": pageSize,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@router.post("/api/public/customer/browse-history", status_code=status.HTTP_201_CREATED)
|
||||
def save_customer_browse_history(
|
||||
body: BrowseHistoryCreateIn,
|
||||
customer: Customer = Depends(require_customer),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
source = browse_source(body.itemType, body.itemId, db)
|
||||
item = db.scalar(
|
||||
select(CustomerBrowseHistory).where(
|
||||
CustomerBrowseHistory.customerId == customer.id,
|
||||
CustomerBrowseHistory.itemType == body.itemType,
|
||||
CustomerBrowseHistory.itemId == body.itemId,
|
||||
)
|
||||
)
|
||||
now = utc_now()
|
||||
if item:
|
||||
item.title = source.title
|
||||
item.image = source.image or ""
|
||||
item.visitedAt = now
|
||||
item.updatedAt = now
|
||||
else:
|
||||
item = CustomerBrowseHistory(
|
||||
customerId=customer.id,
|
||||
itemType=body.itemType,
|
||||
itemId=body.itemId,
|
||||
title=source.title,
|
||||
image=source.image or "",
|
||||
visitedAt=now,
|
||||
createdAt=now,
|
||||
updatedAt=now,
|
||||
)
|
||||
db.add(item)
|
||||
db.commit()
|
||||
db.refresh(item)
|
||||
return success_response(public_browse_history_dict(item), status_code=status.HTTP_201_CREATED)
|
||||
|
||||
|
||||
@router.post("/api/public/leads", status_code=status.HTTP_201_CREATED)
|
||||
def create_lead(
|
||||
body: LeadCreateIn,
|
||||
|
||||
@@ -10,6 +10,7 @@ LeadStatus = Literal["new", "assigned", "contacted", "planning", "won", "invalid
|
||||
LeadType = Literal["general", "vehicle"]
|
||||
VehicleServiceType = Literal["charter", "transfer"]
|
||||
CharterDuration = Literal["halfDay", "fullDay"]
|
||||
BrowseHistoryType = Literal["wanfa-route", "team-building", "wild-archive"]
|
||||
|
||||
|
||||
class LoginIn(BaseModel):
|
||||
@@ -126,6 +127,19 @@ class LeadCreateIn(BaseModel):
|
||||
return self
|
||||
|
||||
|
||||
class BrowseHistoryCreateIn(BaseModel):
|
||||
itemType: BrowseHistoryType
|
||||
itemId: str = Field(min_length=1, max_length=128)
|
||||
|
||||
@field_validator("itemId")
|
||||
@classmethod
|
||||
def normalize_item_id(cls, value: str) -> str:
|
||||
normalized = value.strip()
|
||||
if not normalized:
|
||||
raise ValueError("内容 ID 不能为空")
|
||||
return normalized
|
||||
|
||||
|
||||
class LeadStatusIn(BaseModel):
|
||||
status: LeadStatus
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@ class CurrentSchemaInspector:
|
||||
"AuditLog",
|
||||
"ConciergeAdvisor",
|
||||
"Customer",
|
||||
"CustomerBrowseHistory",
|
||||
"DetailRecord",
|
||||
"HeroSlide",
|
||||
"HomeTeamBuilding",
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
from importlib.util import module_from_spec, spec_from_file_location
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
MIGRATION_PATH = Path(__file__).parents[1] / "alembic" / "versions" / "0036_customer_browse_history.py"
|
||||
SPEC = spec_from_file_location("customer_browse_history_migration", MIGRATION_PATH)
|
||||
assert SPEC and SPEC.loader
|
||||
MIGRATION = module_from_spec(SPEC)
|
||||
SPEC.loader.exec_module(MIGRATION)
|
||||
|
||||
|
||||
def test_customer_browse_history_migration_is_chained_after_current_head():
|
||||
assert MIGRATION.revision == "0036_customer_browse_history"
|
||||
assert MIGRATION.down_revision == "0035_concierge_details_jsonb"
|
||||
|
||||
|
||||
def test_customer_browse_history_table_and_indexes_are_created_only_when_missing():
|
||||
assert MIGRATION.should_create_table(set()) is True
|
||||
assert MIGRATION.should_create_table({MIGRATION.TABLE_NAME}) is False
|
||||
assert MIGRATION.CUSTOMER_HISTORY_INDEXES == {
|
||||
"ix_CustomerBrowseHistory_customerId",
|
||||
"ix_CustomerBrowseHistory_customer_visitedAt",
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
from datetime import datetime
|
||||
from types import SimpleNamespace
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from app.auth import require_customer
|
||||
from app.database import get_db
|
||||
from app.main import create_app
|
||||
from app.models import Customer, Lead, WanfaRoute
|
||||
|
||||
|
||||
class FakeScalarResult:
|
||||
def __init__(self, items):
|
||||
self.items = items
|
||||
|
||||
def all(self):
|
||||
return self.items
|
||||
|
||||
|
||||
class FakeDb:
|
||||
def __init__(self, *, scalar_results=None, scalar_values=None):
|
||||
self.scalar_results = list(scalar_results or [])
|
||||
self.scalar_values = list(scalar_values or [])
|
||||
self.added = []
|
||||
self.committed = False
|
||||
|
||||
def scalars(self, _statement):
|
||||
return FakeScalarResult(self.scalar_results.pop(0) if self.scalar_results else [])
|
||||
|
||||
def scalar(self, _statement):
|
||||
return self.scalar_values.pop(0) if self.scalar_values else None
|
||||
|
||||
def add(self, item):
|
||||
self.added.append(item)
|
||||
|
||||
def flush(self):
|
||||
for item in self.added:
|
||||
if not getattr(item, "id", None):
|
||||
item.id = "browse-history-test"
|
||||
|
||||
def commit(self):
|
||||
self.committed = True
|
||||
|
||||
def refresh(self, _item):
|
||||
return None
|
||||
|
||||
|
||||
def authenticated_app(fake_db, customer):
|
||||
app = create_app()
|
||||
app.dependency_overrides[get_db] = lambda: fake_db
|
||||
app.dependency_overrides[require_customer] = lambda: customer
|
||||
return app
|
||||
|
||||
|
||||
def make_vehicle_lead(**overrides):
|
||||
return Lead(
|
||||
id=overrides.get("id", "lead-1"),
|
||||
leadType="vehicle",
|
||||
contactName=overrides.get("contactName", "Damon"),
|
||||
customerId=overrides.get("customerId", "customer-1"),
|
||||
destination=overrides.get("destination", "黄果树景区"),
|
||||
phone=overrides.get("phone", "13800000000"),
|
||||
travelDate=overrides.get("travelDate", datetime(2026, 8, 25)),
|
||||
peopleCount=overrides.get("peopleCount", 5),
|
||||
note=overrides.get("note", "需要儿童座椅"),
|
||||
vehicleDemand=overrides.get(
|
||||
"vehicleDemand",
|
||||
{
|
||||
"serviceType": "charter",
|
||||
"charterDuration": "fullDay",
|
||||
"travelDate": "2026-08-25",
|
||||
"pickupLocation": "贵阳北站",
|
||||
"dropoffLocation": "黄果树景区",
|
||||
"peopleCount": 5,
|
||||
"luggageCount": 2,
|
||||
"vehicleOptionTitle": "9座精品商务车",
|
||||
"specialRequirements": "需要儿童座椅",
|
||||
},
|
||||
),
|
||||
status=overrides.get("status", "new"),
|
||||
createdAt=overrides.get("createdAt", datetime(2026, 8, 20, 10, 0)),
|
||||
updatedAt=overrides.get("updatedAt", datetime(2026, 8, 20, 10, 0)),
|
||||
)
|
||||
|
||||
|
||||
def test_public_vehicle_demands_are_paginated_and_phone_is_masked():
|
||||
customer = Customer(id="customer-1", phone="13800000000")
|
||||
lead = make_vehicle_lead()
|
||||
db = FakeDb(scalar_results=[[lead]], scalar_values=[1])
|
||||
app = authenticated_app(db, customer)
|
||||
|
||||
try:
|
||||
response = TestClient(app).get(
|
||||
"/api/public/customer/vehicle-demands?pageNum=2&pageSize=1"
|
||||
)
|
||||
finally:
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": {
|
||||
"items": [
|
||||
{
|
||||
"id": "lead-1",
|
||||
"status": "new",
|
||||
"contactName": "Damon",
|
||||
"phoneMasked": "138****0000",
|
||||
"destination": "黄果树景区",
|
||||
"travelDate": "2026-08-25T00:00:00",
|
||||
"peopleCount": 5,
|
||||
"note": "需要儿童座椅",
|
||||
"vehicleDemand": lead.vehicleDemand,
|
||||
"createdAt": "2026-08-20T10:00:00",
|
||||
"updatedAt": "2026-08-20T10:00:00",
|
||||
}
|
||||
],
|
||||
"total": 1,
|
||||
"pageNum": 2,
|
||||
"pageSize": 1,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_public_vehicle_demand_detail_only_returns_the_current_customer_record():
|
||||
customer = Customer(id="customer-1", phone="13800000000")
|
||||
lead = make_vehicle_lead()
|
||||
db = FakeDb(scalar_values=[lead])
|
||||
app = authenticated_app(db, customer)
|
||||
|
||||
try:
|
||||
response = TestClient(app).get("/api/public/customer/vehicle-demands/lead-1")
|
||||
finally:
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["data"]["id"] == "lead-1"
|
||||
assert response.json()["data"]["phoneMasked"] == "138****0000"
|
||||
assert "phone" not in response.json()["data"]
|
||||
|
||||
|
||||
def test_public_vehicle_demand_detail_hides_another_customers_record():
|
||||
customer = Customer(id="customer-1", phone="13800000000")
|
||||
db = FakeDb(scalar_values=[None])
|
||||
app = authenticated_app(db, customer)
|
||||
|
||||
try:
|
||||
response = TestClient(app).get("/api/public/customer/vehicle-demands/lead-other")
|
||||
finally:
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
def test_public_browse_history_is_paginated_for_the_current_customer():
|
||||
customer = Customer(id="customer-1", phone="13800000000")
|
||||
history = SimpleNamespace(
|
||||
id="history-1",
|
||||
customerId="customer-1",
|
||||
itemType="wanfa-route",
|
||||
itemId="family-water",
|
||||
title="亲子玩水",
|
||||
image="https://example.test/family-water.jpg",
|
||||
visitedAt=datetime(2026, 8, 26, 12, 0),
|
||||
)
|
||||
db = FakeDb(scalar_results=[[history]], scalar_values=[1])
|
||||
app = authenticated_app(db, customer)
|
||||
|
||||
try:
|
||||
response = TestClient(app).get(
|
||||
"/api/public/customer/browse-history?pageNum=1&pageSize=20"
|
||||
)
|
||||
finally:
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["data"] == {
|
||||
"items": [
|
||||
{
|
||||
"id": "history-1",
|
||||
"itemType": "wanfa-route",
|
||||
"itemId": "family-water",
|
||||
"title": "亲子玩水",
|
||||
"image": "https://example.test/family-water.jpg",
|
||||
"visitedAt": "2026-08-26T12:00:00",
|
||||
}
|
||||
],
|
||||
"total": 1,
|
||||
"pageNum": 1,
|
||||
"pageSize": 20,
|
||||
}
|
||||
|
||||
|
||||
def test_public_browse_history_upserts_a_valid_route_without_accepting_a_customer_id():
|
||||
customer = Customer(id="customer-1", phone="13800000000")
|
||||
route = WanfaRoute(
|
||||
id="family-water",
|
||||
categoryId="family-route",
|
||||
title="亲子玩水",
|
||||
subtitle="贵州·轻松节奏与自然课堂",
|
||||
image="https://example.test/family-water.jpg",
|
||||
routeCount=4,
|
||||
demandKeyword="亲子玩水",
|
||||
sortOrder=0,
|
||||
createdAt=datetime(2026, 1, 1),
|
||||
updatedAt=datetime(2026, 1, 2),
|
||||
)
|
||||
db = FakeDb(scalar_values=[route, None])
|
||||
app = authenticated_app(db, customer)
|
||||
|
||||
try:
|
||||
response = TestClient(app).post(
|
||||
"/api/public/customer/browse-history",
|
||||
json={
|
||||
"customerId": "customer-other",
|
||||
"itemType": "wanfa-route",
|
||||
"itemId": "family-water",
|
||||
},
|
||||
)
|
||||
finally:
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
assert response.status_code == 201
|
||||
history = db.added[0]
|
||||
assert history.customerId == "customer-1"
|
||||
assert history.itemType == "wanfa-route"
|
||||
assert history.itemId == "family-water"
|
||||
assert history.title == "亲子玩水"
|
||||
assert history.image == "https://example.test/family-water.jpg"
|
||||
assert response.json()["data"]["itemId"] == "family-water"
|
||||
|
||||
|
||||
def test_public_browse_history_updates_existing_item_instead_of_creating_a_duplicate():
|
||||
customer = Customer(id="customer-1", phone="13800000000")
|
||||
route = WanfaRoute(
|
||||
id="family-water",
|
||||
categoryId="family-route",
|
||||
title="亲子玩水·更新",
|
||||
subtitle="贵州·轻松节奏与自然课堂",
|
||||
image="https://example.test/family-water-new.jpg",
|
||||
routeCount=4,
|
||||
demandKeyword="亲子玩水",
|
||||
sortOrder=0,
|
||||
createdAt=datetime(2026, 1, 1),
|
||||
updatedAt=datetime(2026, 1, 2),
|
||||
)
|
||||
history = SimpleNamespace(
|
||||
id="history-1",
|
||||
customerId="customer-1",
|
||||
itemType="wanfa-route",
|
||||
itemId="family-water",
|
||||
title="亲子玩水",
|
||||
image="https://example.test/family-water-old.jpg",
|
||||
visitedAt=datetime(2026, 8, 25, 12, 0),
|
||||
)
|
||||
db = FakeDb(scalar_values=[route, history])
|
||||
app = authenticated_app(db, customer)
|
||||
|
||||
try:
|
||||
response = TestClient(app).post(
|
||||
"/api/public/customer/browse-history",
|
||||
json={"itemType": "wanfa-route", "itemId": "family-water"},
|
||||
)
|
||||
finally:
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
assert response.status_code == 201
|
||||
assert db.added == []
|
||||
assert history.title == "亲子玩水·更新"
|
||||
assert history.image == "https://example.test/family-water-new.jpg"
|
||||
assert history.visitedAt > datetime(2026, 8, 25, 12, 0)
|
||||
Reference in new issue
Block a user