diff --git a/app/api/routers/rbac_service_market.py b/app/api/routers/rbac_service_market.py index eb7b9f3..6e712ca 100644 --- a/app/api/routers/rbac_service_market.py +++ b/app/api/routers/rbac_service_market.py @@ -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,