修复:服务市场接口全线snake_case转camelCase,修复服务方信息加载

- 添加_to_camel通用转换函数,将所有snake_case字段转为camelCase
- 服务列表接口:添加LEFT JOIN talent_profiles,组装talent对象(userId/displayName/avatar/headline/bio)
- 服务详情接口:添加contentHtml字段转换,组装talent对象
- myServices接口:返回完整字段(category/description/contentHtml/paymentMethods等),支持编辑功能
- 修复前端无法正确读取opcName/publisherType/salesCount等camelCase字段的问题
This commit is contained in:
Pine
2026-09-05 17:30:04 +08:00
parent 7cfc0163e0
commit 2cb93b7751
+77 -15
View File
@@ -11,7 +11,7 @@ import time
import uuid
from typing import Any
from fastapi import APIRouter, Depends, HTTPException, Request
from fastapi import APIRouter, Depends, File, HTTPException, Request, UploadFile
from sqlalchemy import text
from ..dependencies import Database, get_current_user, get_db, optional_current_user
@@ -52,6 +52,19 @@ def _parse_json(val: str | None, default: Any) -> Any:
return default
def _to_camel(d: dict) -> dict:
"""将 dict 中的 snake_case key 转换为 camelCase。"""
result = {}
for k, v in d.items():
if "_" in k and not k.startswith("_"):
parts = k.split("_")
camel_key = parts[0] + "".join(p.capitalize() for p in parts[1:])
result[camel_key] = v
else:
result[k] = v
return result
# ── 服务分类 ──────────────────────────────────────────────────────────
SERVICE_CATEGORIES = [
@@ -132,12 +145,15 @@ async def service_list(
# 分页查询
offset = (page - 1) * page_size
list_sql = f"""
SELECT id, opc_id, opc_name, title, category, description, cover,
price, delivery_days, tags, status, rating, order_count,
sales_count, favorite_count, view_count, publisher_type,
payment_methods, region, service_mode, is_recommended, is_hot,
pricing_mode, created_at
FROM opc_services
SELECT s.id, s.opc_id, s.opc_name, s.title, s.category, s.description, s.cover,
s.price, s.delivery_days, s.tags, s.status, s.rating, s.order_count,
s.sales_count, s.favorite_count, s.view_count, s.publisher_type,
s.payment_methods, s.region, s.service_mode, s.is_recommended, s.is_hot,
s.pricing_mode, s.created_at,
t.display_name as talent_name, t.avatar as talent_avatar,
t.headline as talent_headline, t.bio as talent_bio
FROM opc_services s
LEFT JOIN talent_profiles t ON s.opc_id = t.user_id
WHERE {where}
ORDER BY {order_by}
LIMIT :limit OFFSET :offset
@@ -149,8 +165,17 @@ async def service_list(
items = []
for row in rows:
d = _row_to_dict(row)
d["paymentMethods"] = _parse_json(d.get("payment_methods"), [])
d["gallery"] = _parse_json(d.get("gallery_json"), [])
d = _to_camel(d)
d["paymentMethods"] = _parse_json(d.get("paymentMethods") or d.get("payment_methods"), [])
d["gallery"] = _parse_json(d.get("galleryJson") or d.get("gallery_json"), [])
# 组装 talent 对象
d["talent"] = {
"userId": d.get("opcId"),
"displayName": d.get("talentName") or d.get("opcName"),
"avatar": d.get("talentAvatar"),
"headline": d.get("talentHeadline"),
"bio": d.get("talentBio"),
}
items.append(d)
result = {
@@ -165,6 +190,19 @@ async def service_list(
return result
# ── 文件上传(服务封面/图集) ─────────────────────────────────────────
@router.post("/upload", summary="上传服务封面/图集图片")
async def upload_service_media(
file: UploadFile = File(...),
user: dict = Depends(get_current_user),
):
"""上传服务封面或图集图片,返回 OSS/CDN 直链 URL。普通登录用户即可使用。"""
from ...services import media_upload
url = await media_upload.save_media(file, dir="services")
return {"ok": True, "url": url}
# ── 服务详情(增强版:浏览量自增、评价统计、收藏状态) ───────────────
@router.get("/services/{service_id}", summary="服务详情(增强版:浏览量/评价统计/收藏状态)")
@@ -184,13 +222,26 @@ async def service_detail(
if row is None:
raise HTTPException(status_code=404, detail="服务不存在")
item = _row_to_dict(row)
item = _to_camel(item)
if item.get("status") != "published":
raise HTTPException(status_code=404, detail="服务不存在或已下架")
# 解析 JSON 字段
item["paymentMethods"] = _parse_json(item.get("payment_methods"), [])
item["gallery"] = _parse_json(item.get("gallery_json"), [])
item["skus"] = _parse_json(item.get("skus_json"), [])
item["paymentMethods"] = _parse_json(item.get("paymentMethods") or item.get("payment_methods"), [])
item["gallery"] = _parse_json(item.get("galleryJson") or item.get("gallery_json"), [])
item["skus"] = _parse_json(item.get("skusJson") or item.get("skus_json"), [])
# 字段转换:snake_case -> camelCase
item["contentHtml"] = item.get("contentHtml") or item.get("content_html") or ""
# 组装 talent 对象
item["talent"] = {
"userId": item.get("opcId"),
"displayName": item.get("talentName") or item.get("opcName"),
"avatar": item.get("talentAvatar"),
"headline": item.get("talentHeadline"),
"bio": item.get("talentBio"),
}
# 浏览量自增(同步执行,避免异步任务导致会话状态冲突)
try:
@@ -1061,8 +1112,11 @@ async def my_services(
rows = (await db.session.execute(
text(f"""
SELECT id, title, cover, price, status, rating, sales_count,
view_count, favorite_count, publisher_type, created_at, updated_at
SELECT id, opc_id, opc_name, title, category, description, cover,
price, delivery_days, tags, status, rating, sales_count,
view_count, favorite_count, publisher_type, payment_methods,
content_html, service_mode, pricing_mode, region, refund_policy,
created_at, updated_at
FROM opc_services
WHERE {where}
ORDER BY created_at DESC
@@ -1071,8 +1125,16 @@ async def my_services(
{**params, "limit": page_size, "offset": offset},
)).fetchall()
items = []
for row in rows:
d = _row_to_dict(row)
d = _to_camel(d)
d["paymentMethods"] = _parse_json(d.get("paymentMethods") or d.get("payment_methods"), [])
d["contentHtml"] = d.get("contentHtml") or d.get("content_html") or ""
items.append(d)
return {
"items": [_row_to_dict(r) for r in rows],
"items": items,
"total": count_row[0] if count_row else 0,
"page": page,
"page_size": page_size,