refactor(server): 移除鉴权并归档遗留路由至 legacy/
- 删除 auth.py 与 deps.py,各路由去除 get_current_user 依赖 - collection.py 更名为 materials.py,冻结链路(ozon/publish/shops/categories)移入 legacy/ - 扩展默认生图服务端口并入 8800 并自动迁移旧配置,水印默认文案改为 Panda Store - 新增 docs/v2.1/backend-structure.md 后端结构盘点文档
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
"""Ozon Seller API 客户端(薄封装:鉴权头 + 错误映射 + 退避)。"""
|
||||
# ⚠️ 冻结代码(Ozon API 直传链路):保留不维护,V2.1 起接口保留但不再投入。
|
||||
from __future__ import annotations
|
||||
|
||||
import httpx
|
||||
|
||||
OZON_BASE_URL = "https://api-seller.ozon.ru"
|
||||
|
||||
|
||||
class OzonAPIError(Exception):
|
||||
def __init__(self, status: int, detail: str):
|
||||
self.status = status
|
||||
self.detail = detail
|
||||
super().__init__(f"Ozon API {status}: {detail}")
|
||||
|
||||
|
||||
class OzonClient:
|
||||
def __init__(self, client_id: str, api_key: str, base_url: str = OZON_BASE_URL):
|
||||
self.client_id = client_id
|
||||
self.api_key = api_key
|
||||
self.base_url = base_url
|
||||
|
||||
def _headers(self) -> dict:
|
||||
return {
|
||||
"Client-Id": self.client_id,
|
||||
"Api-Key": self.api_key,
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
async def post(self, path: str, body: dict | None = None, timeout: float = 60.0) -> dict:
|
||||
async with httpx.AsyncClient(timeout=timeout, follow_redirects=True) as client:
|
||||
resp = await client.post(f"{self.base_url}{path}", headers=self._headers(), json=body or {})
|
||||
if resp.status_code >= 400:
|
||||
raise OzonAPIError(resp.status_code, resp.text[:500])
|
||||
try:
|
||||
return resp.json()
|
||||
except Exception: # noqa: BLE001
|
||||
return {}
|
||||
|
||||
async def get(self, path: str, timeout: float = 60.0) -> dict:
|
||||
async with httpx.AsyncClient(timeout=timeout, follow_redirects=True) as client:
|
||||
resp = await client.get(f"{self.base_url}{path}", headers=self._headers())
|
||||
if resp.status_code >= 400:
|
||||
raise OzonAPIError(resp.status_code, resp.text[:500])
|
||||
try:
|
||||
return resp.json()
|
||||
except Exception: # noqa: BLE001
|
||||
return {}
|
||||
|
||||
async def test_credentials(self) -> dict:
|
||||
"""调 /v1/roles 校验凭证与权限范围。"""
|
||||
return await self.post("/v1/roles", {})
|
||||
@@ -0,0 +1,80 @@
|
||||
"""发布:组装 ImportProductsV3 items[0] + 必填校验 + 轮询回填。"""
|
||||
# ⚠️ 冻结代码(Ozon API 直传链路):保留不维护,V2.1 起接口保留但不再投入。
|
||||
from __future__ import annotations
|
||||
|
||||
from models import Product
|
||||
|
||||
|
||||
def _fmt(v) -> str:
|
||||
if v is None:
|
||||
return ""
|
||||
return str(v)
|
||||
|
||||
|
||||
def validate_ready(product: Product) -> list[str]:
|
||||
"""返回缺失/非法必填项的中文提示列表;空列表表示可发布。"""
|
||||
missing: list[str] = []
|
||||
if not product.offer_id.strip():
|
||||
missing.append("货号 offer_id")
|
||||
if not product.name.strip():
|
||||
missing.append("商品名 name")
|
||||
if not product.description.strip():
|
||||
missing.append("描述 description")
|
||||
if not product.description_category_id:
|
||||
missing.append("类目 description_category_id")
|
||||
if product.price is None or product.price <= 0:
|
||||
missing.append("售价 price")
|
||||
if not product.weight or product.weight <= 0:
|
||||
missing.append("重量 weight")
|
||||
for label, val in (("长 depth", product.depth), ("宽 width", product.width), ("高 height", product.height)):
|
||||
if not val or val <= 0:
|
||||
missing.append(label)
|
||||
if not product.images:
|
||||
missing.append("主图 images(至少 1 张)")
|
||||
elif any(u and u.startswith("http://") for u in product.images):
|
||||
missing.append("图片链接必须使用 https(Ozon 不接受 http 直链)")
|
||||
return missing
|
||||
|
||||
|
||||
def _with_model_name(product: Product) -> list:
|
||||
"""把 raw.model_name 自动注入为 attribute 9048(型号名称),用于多变体合并。"""
|
||||
attrs = list(product.attributes or [])
|
||||
model_name = (product.raw or {}).get("model_name") if product.raw else None
|
||||
if not model_name:
|
||||
return attrs
|
||||
# 已手动映射 9048 就不重复添加
|
||||
for a in attrs:
|
||||
if isinstance(a, dict) and a.get("id") == 9048:
|
||||
return attrs
|
||||
attrs.append({"complex_id": 0, "id": 9048, "values": [{"value": model_name}]})
|
||||
return attrs
|
||||
|
||||
|
||||
def build_import_item(product: Product) -> dict:
|
||||
item: dict = {
|
||||
"offer_id": product.offer_id,
|
||||
"name": product.name,
|
||||
"description": product.description,
|
||||
"description_category_id": product.description_category_id,
|
||||
"price": _fmt(product.price),
|
||||
"old_price": _fmt(product.old_price),
|
||||
"currency_code": product.currency_code or "CNY",
|
||||
"vat": product.vat or "0",
|
||||
"depth": product.depth,
|
||||
"width": product.width,
|
||||
"height": product.height,
|
||||
"dimension_unit": product.dimension_unit or "mm",
|
||||
"weight": product.weight,
|
||||
"weight_unit": product.weight_unit or "g",
|
||||
"images": product.images or [],
|
||||
"primary_image": product.primary_image or "",
|
||||
"images360": product.images360 or [],
|
||||
"color_image": product.color_image or "",
|
||||
"attributes": _with_model_name(product),
|
||||
"complex_attributes": product.complex_attributes or [],
|
||||
}
|
||||
if product.type_id:
|
||||
item["type_id"] = product.type_id
|
||||
if product.barcode:
|
||||
item["barcode"] = product.barcode
|
||||
return item
|
||||
Reference in New Issue
Block a user