feat(api): 实现三端统一的JSON API响应契约

- 新增`api_response.py`统一响应封装工具类,提供标准成功/错误响应构造方法
- 重构WonderQ-Admin全局异常处理器,将所有异常转换为标准响应格式
- 修改所有公共和管理端接口的返回逻辑,统一使用`code`(与HTTP状态码一致)、`msg`和`data`的三层结构
- 新增`api-response-contract.md`文档,定义完整的三端统一JSON响应规范
- 更新所有领域API文档,明确业务数据需位于`data`字段内,补充响应格式说明
- 为WonderQ-MiniAPP和WonderQ-Admin-UI新增响应解析逻辑和类型定义,自动完成协议校验和错误处理
- 更新所有测试用例,适配新的响应结构确保接口符合契约要求
- 新增`module-config-api.md`模块配置API文档,补充站点模块配置的接口约定
- 更新项目README文档,调整文档分类顺序将响应契约置于首位
This commit is contained in:
duanshuwen committed 2026-08-19 22:02:20 +08:00
1 parent d16924584c
commit e082bd2d98
28 files changed
+993 -383

No files matched your search

+79 -74
View File
@@ -11,6 +11,7 @@ from fastapi import APIRouter, Depends, File, Form, HTTPException, Query, Reques
from sqlalchemy import func, or_, select
from sqlalchemy.orm import Session, selectinload
from ..auth import create_token, get_actor_id, require_admin, verify_password
from ..api_response import success_response
from ..config import get_settings
from ..database import get_db
from ..models import (
@@ -503,7 +504,7 @@ def create_home_item(db: Session, request: Request, body, model, entity: str):
after = model_dict(item)
audit(db, get_actor_id(request), "create", entity, item.id, after=after)
db.commit()
return after
return success_response(after, status_code=status.HTTP_201_CREATED)
def update_home_item(db: Session, request: Request, item_id: str, body, model, entity: str, label: str, code: str):
@@ -515,7 +516,7 @@ def update_home_item(db: Session, request: Request, item_id: str, body, model, e
after = model_dict(item)
audit(db, get_actor_id(request), "update", entity, item.id, after=after, before=before)
db.commit()
return after
return success_response(after)
def delete_home_item(db: Session, request: Request, item_id: str, model, entity: str, label: str, code: str):
@@ -528,7 +529,7 @@ def delete_home_item(db: Session, request: Request, item_id: str, model, entity:
result = {"id": item_id}
audit(db, get_actor_id(request), "delete", entity, item_id, before=before, after=result)
db.commit()
return result
return success_response(result)
def reorder_home_items(db: Session, request: Request, body: HomeReorderIn, model, entity: str):
@@ -539,12 +540,12 @@ def reorder_home_items(db: Session, request: Request, body: HomeReorderIn, model
after = [model_dict(item) for item in ordered]
audit(db, get_actor_id(request), "reorder", entity, after=after)
db.commit()
return {"items": after}
return success_response({"items": after})
@router.get("/home/experiences")
def list_home_experiences(_user: AdminUser = Depends(require_admin), db: Session = Depends(get_db)):
return {"items": [model_dict(item) for item in home_items(db, HomeExperience)]}
return success_response({"items": [model_dict(item) for item in home_items(db, HomeExperience)]})
@router.post("/home/experiences", status_code=status.HTTP_201_CREATED)
@@ -590,7 +591,7 @@ def delete_home_experience(
@router.get("/home/team-buildings")
def list_home_team_buildings(_user: AdminUser = Depends(require_admin), db: Session = Depends(get_db)):
return {"items": [model_dict(item) for item in home_items(db, HomeTeamBuilding)]}
return success_response({"items": [model_dict(item) for item in home_items(db, HomeTeamBuilding)]})
@router.post("/home/team-buildings", status_code=status.HTTP_201_CREATED)
@@ -636,7 +637,7 @@ def delete_home_team_building(
@router.get("/home/wild-archives")
def list_home_wild_archives(_user: AdminUser = Depends(require_admin), db: Session = Depends(get_db)):
return {"items": [model_dict(item) for item in home_items(db, HomeWildArchive)]}
return success_response({"items": [model_dict(item) for item in home_items(db, HomeWildArchive)]})
@router.post("/home/wild-archives", status_code=status.HTTP_201_CREATED)
@@ -713,7 +714,7 @@ def ensure_home_wanfa_category_available(
@router.get("/home/play-recommendations")
def list_home_wanfa_recommendations(_user: AdminUser = Depends(require_admin), db: Session = Depends(get_db)):
return {"items": [home_wanfa_recommendation_dict(item) for item in home_wanfa_recommendation_items(db)]}
return success_response({"items": [home_wanfa_recommendation_dict(item) for item in home_wanfa_recommendation_items(db)]})
@router.post("/home/play-recommendations", status_code=status.HTTP_201_CREATED)
@@ -736,7 +737,7 @@ def create_home_wanfa_recommendation(
after = home_wanfa_recommendation_dict(recommendation)
audit(db, get_actor_id(request), "create", "home_wanfa_recommendation", recommendation.id, after=after)
db.commit()
return after
return success_response(after, status_code=status.HTTP_201_CREATED)
@router.patch("/home/play-recommendations/reorder")
@@ -753,7 +754,7 @@ def reorder_home_wanfa_recommendations(
after = [home_wanfa_recommendation_dict(item) for item in ordered]
audit(db, get_actor_id(request), "reorder", "home_wanfa_recommendation", after=after)
db.commit()
return {"items": after}
return success_response({"items": after})
@router.patch("/home/play-recommendations/{recommendation_id}")
@@ -777,7 +778,7 @@ def update_home_wanfa_recommendation(
after = home_wanfa_recommendation_dict(recommendation)
audit(db, get_actor_id(request), "update", "home_wanfa_recommendation", recommendation.id, after=after, before=before)
db.commit()
return after
return success_response(after)
@router.delete("/home/play-recommendations/{recommendation_id}")
@@ -796,7 +797,7 @@ def delete_home_wanfa_recommendation(
result = {"id": recommendation_id}
audit(db, get_actor_id(request), "delete", "home_wanfa_recommendation", recommendation_id, before=before, after=result)
db.commit()
return result
return success_response(result)
def concierge_error(status_code: int, message: str, code: str, details: dict | None = None) -> None:
@@ -847,7 +848,7 @@ def validate_detail_order(item_ids: list[str], current_ids: list[str]) -> None:
@router.get("/details")
def list_details(_user: AdminUser = Depends(require_admin), db: Session = Depends(get_db)):
details = db.scalars(select(DetailRecord).order_by(DetailRecord.sortOrder.asc())).all()
return {"details": [detail_record_dict(detail) for detail in details]}
return success_response({"details": [detail_record_dict(detail) for detail in details]})
@router.patch("/details/reorder")
@@ -864,12 +865,12 @@ def reorder_details(
after = [detail_record_dict(detail) for detail in ordered]
audit(db, get_actor_id(request), "reorder", "detail_record", after=after)
db.commit()
return {"items": after}
return success_response({"items": after})
@router.get("/details/{detail_id}")
def get_detail(detail_id: str, _user: AdminUser = Depends(require_admin), db: Session = Depends(get_db)):
return detail_record_dict(detail_or_error(db, detail_id))
return success_response(detail_record_dict(detail_or_error(db, detail_id)))
@router.post("/details", status_code=status.HTTP_201_CREATED)
@@ -889,7 +890,7 @@ def create_detail(
after = detail_record_dict(detail)
audit(db, get_actor_id(request), "create", "detail_record", detail.id, after=after)
db.commit()
return after
return success_response(after, status_code=status.HTTP_201_CREATED)
@router.patch("/details/{detail_id}")
@@ -910,7 +911,7 @@ def update_detail(
after = detail_record_dict(detail)
audit(db, get_actor_id(request), "update", "detail_record", detail.id, after=after, before=before)
db.commit()
return after
return success_response(after)
@router.delete("/details/{detail_id}")
@@ -928,7 +929,7 @@ def delete_detail(
item.sortOrder = index
audit(db, get_actor_id(request), "delete", "detail_record", detail.id, before=before)
db.commit()
return {"id": detail.id}
return success_response({"id": detail.id})
@router.get("/wanfa/categories")
@@ -936,7 +937,7 @@ def list_wanfa_categories(_user: AdminUser = Depends(require_admin), db: Session
categories = db.scalars(
select(WanfaCategory).options(selectinload(WanfaCategory.routes)).order_by(WanfaCategory.sortOrder.asc())
).all()
return {"categories": [wanfa_category_dict(category) for category in categories]}
return success_response({"categories": [wanfa_category_dict(category) for category in categories]})
@router.post("/wanfa/categories", status_code=status.HTTP_201_CREATED)
@@ -952,7 +953,7 @@ def create_wanfa_category(
after = wanfa_category_dict(category)
audit(db, get_actor_id(request), "create", "wanfa_category", category.id, after=after)
db.commit()
return after
return success_response(after, status_code=status.HTTP_201_CREATED)
@router.patch("/wanfa/categories/reorder")
@@ -969,7 +970,7 @@ def reorder_wanfa_categories(
after = [wanfa_category_dict(category) for category in ordered]
audit(db, get_actor_id(request), "reorder", "wanfa_category", after=after)
db.commit()
return {"items": after}
return success_response({"items": after})
@router.patch("/wanfa/categories/{category_id}")
@@ -988,7 +989,7 @@ def update_wanfa_category(
after = wanfa_category_dict(category)
audit(db, get_actor_id(request), "update", "wanfa_category", category.id, after=after, before=before)
db.commit()
return after
return success_response(after)
@router.delete("/wanfa/categories/{category_id}")
@@ -1017,7 +1018,7 @@ def delete_wanfa_category(
db.delete(category)
audit(db, get_actor_id(request), "delete", "wanfa_category", category.id, before=before)
db.commit()
return {"id": category.id}
return success_response({"id": category.id})
@router.post("/wanfa/categories/{category_id}/routes", status_code=status.HTTP_201_CREATED)
@@ -1046,7 +1047,7 @@ def create_wanfa_route(
after = wanfa_route_dict(route)
audit(db, get_actor_id(request), "create", "wanfa_route", route.id, after=after)
db.commit()
return after
return success_response(after, status_code=status.HTTP_201_CREATED)
@router.patch("/wanfa/categories/{category_id}/routes/reorder")
@@ -1065,7 +1066,7 @@ def reorder_wanfa_routes(
after = [wanfa_route_dict(route) for route in ordered]
audit(db, get_actor_id(request), "reorder", "wanfa_route", category.id, after=after)
db.commit()
return {"items": after}
return success_response({"items": after})
@router.patch("/wanfa/categories/{category_id}/routes/{route_id}")
@@ -1087,7 +1088,7 @@ def update_wanfa_route(
after = wanfa_route_dict(route)
audit(db, get_actor_id(request), "update", "wanfa_route", route.id, after=after, before=before)
db.commit()
return after
return success_response(after)
@router.delete("/wanfa/categories/{category_id}/routes/{route_id}")
@@ -1104,13 +1105,13 @@ def delete_wanfa_route(
db.delete(route)
audit(db, get_actor_id(request), "delete", "wanfa_route", route.id, before=before)
db.commit()
return {"id": route.id}
return success_response({"id": route.id})
@router.get("/concierge/advisors")
def list_concierge_advisors(_user: AdminUser = Depends(require_admin), db: Session = Depends(get_db)):
advisors = db.scalars(select(ConciergeAdvisor).order_by(ConciergeAdvisor.sortOrder.asc())).all()
return {"advisors": [concierge_advisor_dict(advisor) for advisor in advisors]}
return success_response({"advisors": [concierge_advisor_dict(advisor) for advisor in advisors]})
@router.post("/concierge/advisors", status_code=status.HTTP_201_CREATED)
@@ -1130,7 +1131,7 @@ def create_concierge_advisor(
after = concierge_advisor_dict(advisor)
audit(db, get_actor_id(request), "create", "concierge_advisor", advisor.id, after=after)
db.commit()
return after
return success_response(after, status_code=status.HTTP_201_CREATED)
@router.patch("/concierge/advisors/reorder")
@@ -1147,7 +1148,7 @@ def reorder_concierge_advisors(
after = [concierge_advisor_dict(advisor) for advisor in ordered]
audit(db, get_actor_id(request), "reorder", "concierge_advisor", after=after)
db.commit()
return {"items": after}
return success_response({"items": after})
@router.patch("/concierge/advisors/{advisor_id}")
@@ -1169,7 +1170,7 @@ def update_concierge_advisor(
after = concierge_advisor_dict(advisor)
audit(db, get_actor_id(request), "update", "concierge_advisor", advisor.id, after=after, before=before)
db.commit()
return after
return success_response(after)
@router.delete("/concierge/advisors/{advisor_id}")
@@ -1189,7 +1190,7 @@ def delete_concierge_advisor(
result = {"id": advisor_id}
audit(db, get_actor_id(request), "delete", "concierge_advisor", advisor_id, before=before, after=result)
db.commit()
return result
return success_response(result)
@router.post("/auth/login")
@@ -1197,15 +1198,17 @@ def login(body: LoginIn, db: Session = Depends(get_db)):
user = db.scalar(select(AdminUser).where(AdminUser.email == body.email))
if not user or not user.isActive or not verify_password(body.password, user.passwordHash):
raise HTTPException(status_code=401, detail="账号或密码错误")
return {
"token": create_token(user),
"user": {"id": user.id, "email": user.email, "name": user.name, "role": user.role},
}
return success_response(
{
"token": create_token(user),
"user": {"id": user.id, "email": user.email, "name": user.name, "role": user.role},
}
)
@router.get("/me")
def me(user: AdminUser = Depends(require_admin)):
return {"id": user.id, "email": user.email, "name": user.name, "role": user.role}
return success_response({"id": user.id, "email": user.email, "name": user.name, "role": user.role})
@router.get("/dashboard")
@@ -1217,37 +1220,39 @@ def dashboard(_user: AdminUser = Depends(require_admin), db: Session = Depends(g
recent = db.scalars(
select(Lead).options(selectinload(Lead.assignedUser)).order_by(Lead.createdAt.desc()).limit(5)
).all()
return {"stats": stats, "recentLeads": [lead_dict(lead) for lead in recent]}
return success_response({"stats": stats, "recentLeads": [lead_dict(lead) for lead in recent]})
@router.get("/site-config")
def admin_site_config(_user: AdminUser = Depends(require_admin), db: Session = Depends(get_db)):
return {
"heroSlides": [
hero_slide_admin_dict(item)
for item in db.scalars(select(HeroSlide).order_by(HeroSlide.sortOrder.asc())).all()
],
"destinationHero": [
model_dict(item)
for item in db.scalars(select(DestinationHero).order_by(DestinationHero.sortOrder.asc())).all()
],
"vehicleOptions": [
model_dict(item)
for item in db.scalars(select(VehicleOption).order_by(VehicleOption.sortOrder.asc())).all()
],
"demandHero": [
model_dict(item)
for item in db.scalars(select(DemandHero).order_by(DemandHero.sortOrder.asc())).all()
],
"demandFeatureCards": [
model_dict(item)
for item in db.scalars(select(DemandFeatureCard).order_by(DemandFeatureCard.sortOrder.asc())).all()
],
"demandForm": [
model_dict(item)
for item in db.scalars(select(DemandForm).order_by(DemandForm.createdAt.asc())).all()
],
}
return success_response(
{
"heroSlides": [
hero_slide_admin_dict(item)
for item in db.scalars(select(HeroSlide).order_by(HeroSlide.sortOrder.asc())).all()
],
"destinationHero": [
model_dict(item)
for item in db.scalars(select(DestinationHero).order_by(DestinationHero.sortOrder.asc())).all()
],
"vehicleOptions": [
model_dict(item)
for item in db.scalars(select(VehicleOption).order_by(VehicleOption.sortOrder.asc())).all()
],
"demandHero": [
model_dict(item)
for item in db.scalars(select(DemandHero).order_by(DemandHero.sortOrder.asc())).all()
],
"demandFeatureCards": [
model_dict(item)
for item in db.scalars(select(DemandFeatureCard).order_by(DemandFeatureCard.sortOrder.asc())).all()
],
"demandForm": [
model_dict(item)
for item in db.scalars(select(DemandForm).order_by(DemandForm.createdAt.asc())).all()
],
}
)
@router.post("/site-config/{module}", status_code=status.HTTP_201_CREATED)
@@ -1269,7 +1274,7 @@ def create_site_config(
after = site_item_dict(module, item)
audit(db, get_actor_id(request), "create", config["entity"], item.id, after)
db.commit()
return after
return success_response(after, status_code=status.HTTP_201_CREATED)
@router.patch("/site-config/{module}/reorder")
@@ -1300,7 +1305,7 @@ def reorder_site_config(
after = [site_item_dict(module, item) for item in ordered_items]
audit(db, get_actor_id(request), "reorder", config["entity"], module, {"items": after}, {"items": before})
db.commit()
return {"items": after}
return success_response({"items": after})
@router.patch("/site-config/{module}/{item_id}")
@@ -1322,7 +1327,7 @@ def update_site_config(
after = site_item_dict(module, item)
audit(db, get_actor_id(request), "update", config["entity"], item.id, after, before)
db.commit()
return after
return success_response(after)
@router.delete("/site-config/{module}/{item_id}")
@@ -1347,7 +1352,7 @@ def delete_site_config(
result = {"id": item_id}
audit(db, get_actor_id(request), "delete", config["entity"], item_id, result, before)
db.commit()
return result
return success_response(result)
@router.get("/leads")
@@ -1393,7 +1398,7 @@ def list_leads(
)
).distinct()
leads = db.scalars(stmt).all()
return {"items": [lead_dict(lead) for lead in leads]}
return success_response({"items": [lead_dict(lead) for lead in leads]})
@router.patch("/leads/{lead_id}/status")
@@ -1406,13 +1411,13 @@ def update_lead_status(lead_id: str, body: LeadStatusIn, request: Request, _user
db.flush()
audit(db, get_actor_id(request), "update_status", "lead", lead.id, model_dict(lead), before)
db.commit()
return model_dict(lead)
return success_response(model_dict(lead))
@router.get("/media-assets")
def list_media_assets(_user: AdminUser = Depends(require_admin), db: Session = Depends(get_db)):
assets = db.scalars(select(MediaAsset).order_by(MediaAsset.createdAt.desc()).limit(200)).all()
return {"items": [model_dict(asset) for asset in assets]}
return success_response({"items": [model_dict(asset) for asset in assets]})
@router.post("/media-assets/upload", status_code=status.HTTP_201_CREATED)
@@ -1439,7 +1444,7 @@ def upload_media_asset(
after = model_dict(asset)
audit(db, get_actor_id(request), "upload", "media_asset", asset.id, after)
db.commit()
return after
return success_response(after, status_code=status.HTTP_201_CREATED)
@router.post("/reset-guizhou-content")
@@ -1447,7 +1452,7 @@ def reset_content(request: Request, _user: AdminUser = Depends(require_admin), d
result = reset_guizhou_content(db)
audit(db, get_actor_id(request), "reset_guizhou_content", "site_content", after=result)
db.commit()
return result
return success_response(result)
@router.post("/publish", status_code=status.HTTP_201_CREATED)
@@ -1458,4 +1463,4 @@ def publish(request: Request, _user: AdminUser = Depends(require_admin), db: Ses
db.flush()
audit(db, get_actor_id(request), "publish", "site_version", version.id, model_dict(version))
db.commit()
return model_dict(version)
return success_response(model_dict(version), status_code=status.HTTP_201_CREATED)
+20 -17
View File
@@ -3,6 +3,7 @@ from sqlalchemy import select
from sqlalchemy.orm import Session, selectinload
from .shared import site_config
from ..auth import create_customer_token, require_customer
from ..api_response import success_response
from ..database import get_db
from ..models import ConciergeAdvisor, Customer, DetailRecord, HomeExperience, HomeTeamBuilding, HomeWanfaRecommendation, HomeWildArchive, Lead, WanfaCategory
from ..schemas import LeadCreateIn, PhoneLoginIn
@@ -36,12 +37,12 @@ def public_customer_dict(customer: Customer) -> dict:
@router.get("/health")
def health():
return {"ok": True, "service": "miniapp-api"}
return success_response({"ok": True, "service": "miniapp-api"})
@router.get("/api/public/site-config")
def get_site_config(db: Session = Depends(get_db)):
return site_config(db, active_only=True)
return success_response(site_config(db, active_only=True))
@router.get("/api/public/wanfa/categories")
@@ -51,7 +52,7 @@ def get_public_wanfa_categories(db: Session = Depends(get_db)):
.options(selectinload(WanfaCategory.routes))
.order_by(WanfaCategory.sortOrder.asc())
).all()
return {"categories": [public_wanfa_category_dict(category) for category in categories]}
return success_response({"categories": [public_wanfa_category_dict(category) for category in categories]})
@router.get("/api/public/details/{detail_key}")
@@ -64,7 +65,7 @@ def get_public_detail(detail_key: str, db: Session = Depends(get_db)):
)
if not detail:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="详情不存在")
return public_detail_dict(detail)
return success_response(public_detail_dict(detail))
@router.get("/api/public/concierge/advisors")
@@ -74,7 +75,7 @@ def get_public_concierge_advisors(db: Session = Depends(get_db)):
.where(ConciergeAdvisor.isActive.is_(True))
.order_by(ConciergeAdvisor.sortOrder.asc())
).all()
return {"advisors": [public_concierge_advisor_dict(advisor) for advisor in advisors]}
return success_response({"advisors": [public_concierge_advisor_dict(advisor) for advisor in advisors]})
@router.get("/api/public/home")
@@ -100,12 +101,14 @@ def get_public_home(db: Session = Depends(get_db)):
.where(HomeWanfaRecommendation.isActive.is_(True))
.order_by(HomeWanfaRecommendation.sortOrder.asc())
).all()
return {
"experiences": [public_home_experience_dict(item) for item in experiences],
"teamBuildings": [public_home_team_building_dict(item) for item in team_buildings],
"wildArchives": [public_home_wild_archive_dict(item) for item in wild_archives],
"playRecommendations": [public_home_wanfa_recommendation_dict(item) for item in play_recommendations],
}
return success_response(
{
"experiences": [public_home_experience_dict(item) for item in experiences],
"teamBuildings": [public_home_team_building_dict(item) for item in team_buildings],
"wildArchives": [public_home_wild_archive_dict(item) for item in wild_archives],
"playRecommendations": [public_home_wanfa_recommendation_dict(item) for item in play_recommendations],
}
)
@router.get("/api/public/home/team-buildings/{team_building_id}")
@@ -118,7 +121,7 @@ def get_public_team_building(team_building_id: str, db: Session = Depends(get_db
)
if not team_building:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="团队共创不存在")
return public_home_team_building_detail_dict(team_building)
return success_response(public_home_team_building_detail_dict(team_building))
@router.get("/api/public/home/wild-archives")
@@ -128,7 +131,7 @@ def list_public_wild_archives(db: Session = Depends(get_db)):
.where(HomeWildArchive.isActive.is_(True))
.order_by(HomeWildArchive.sortOrder.asc())
).all()
return {"items": [public_home_wild_archive_dict(item) for item in archives]}
return success_response({"items": [public_home_wild_archive_dict(item) for item in archives]})
@router.get("/api/public/home/wild-archives/{archive_id}")
@@ -141,7 +144,7 @@ def get_public_wild_archive(archive_id: str, db: Session = Depends(get_db)):
)
if not archive:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="客片案例不存在")
return public_home_wild_archive_detail_dict(archive)
return success_response(public_home_wild_archive_detail_dict(archive))
@router.post("/api/public/auth/phone-login")
@@ -160,12 +163,12 @@ def phone_login(body: PhoneLoginIn, db: Session = Depends(get_db)):
db.flush()
db.commit()
db.refresh(customer)
return {"token": create_customer_token(customer), "customer": public_customer_dict(customer)}
return success_response({"token": create_customer_token(customer), "customer": public_customer_dict(customer)})
@router.get("/api/public/auth/me")
def current_customer(customer: Customer = Depends(require_customer)):
return public_customer_dict(customer)
return success_response(public_customer_dict(customer))
@router.post("/api/public/leads", status_code=status.HTTP_201_CREATED)
@@ -174,4 +177,4 @@ def create_lead(body: LeadCreateIn, db: Session = Depends(get_db)):
db.add(lead)
db.commit()
db.refresh(lead)
return {"id": lead.id, "status": lead.status}
return success_response({"id": lead.id, "status": lead.status}, status_code=status.HTTP_201_CREATED)