Files
live-hub-py/web/backend/routers/weibo_collab.py
T

641 lines
20 KiB
Python

"""微博外部单协作接口。"""
from __future__ import annotations
import asyncio
import os
from fastapi import (
APIRouter,
Depends,
HTTPException,
Query,
WebSocket,
WebSocketDisconnect,
)
from sqlalchemy.orm import Session
from ..database import get_db
from ..deps import authenticate_websocket, require_permission
from ..models import User, WeiboAccount, WeiboCollabGoods, WeiboCollabShop
from ..permissions import get_user_permissions
from ..schemas import (
WeiboCollabAccountLookupOut,
WeiboCollabAccountLookupRequest,
WeiboCollabAccountOptionOut,
WeiboCollabAccountRequest,
WeiboCollabArchiveRequest,
WeiboCollabBalanceCheckRequest,
WeiboCollabExchangeCheckRequest,
WeiboCollabGoodsCreate,
WeiboCollabGoodsOut,
WeiboCollabGoodsUpdate,
WeiboCollabMetricOut,
WeiboCollabOrderCreate,
WeiboCollabOrderListOut,
WeiboCollabOrderOut,
WeiboCollabOrderUpdate,
WeiboCollabOwnerOut,
WeiboCollabShopCreate,
WeiboCollabShopOut,
WeiboCollabShopUpdate,
WeiboCollabStatusBatchRequest,
WeiboCollabStatusRequest,
WeiboCollabSummaryOut,
WeiboCollabTimelineOut,
)
from ..services import weibo_collab_service as collab
from ..services.weibo_collab_events import collab_events
router = APIRouter(prefix="/api/weibo", tags=["微博外部单协作"])
WEIBO_DEFAULT_ACTIVITY = os.getenv("WEIBO_DEFAULT_ACTIVITY", "").strip()
def _http_error(exc: collab.CollabError) -> HTTPException:
return HTTPException(status_code=exc.status_code, detail=exc.detail)
def _one(db: Session, order) -> dict:
return collab.serialize_one(db, order)
def _publish_change(current: User, *, scope: str, action: str) -> None:
collab_events.publish(scope=scope, action=action, actor_id=int(current.id))
def _shop_out(shop: WeiboCollabShop) -> dict:
return {
"id": shop.id,
"name": shop.name,
"sort_order": shop.sort_order,
"is_active": shop.is_active,
"remark": shop.remark,
"created_by": shop.created_by,
"updated_by": shop.updated_by,
"created_at": shop.created_at,
"updated_at": shop.updated_at,
}
def _goods_out(goods: WeiboCollabGoods) -> dict:
return {
"id": goods.id,
"name": goods.name,
"price": goods.price,
"goods_id": goods.goods_id,
"goods_image": goods.goods_image,
"sort_order": goods.sort_order,
"is_active": goods.is_active,
"remark": goods.remark,
"created_by": goods.created_by,
"updated_by": goods.updated_by,
"created_at": goods.created_at,
"updated_at": goods.updated_at,
}
@router.websocket("/external-orders/ws")
async def ws_external_orders(websocket: WebSocket):
user = authenticate_websocket(websocket)
if not user or "weibo:collab:view" not in get_user_permissions(user):
await websocket.close(code=1008, reason="未授权")
return
await websocket.accept()
queue = collab_events.subscribe()
try:
await websocket.send_json(collab_events.ready_event())
while True:
try:
event = await asyncio.wait_for(queue.get(), timeout=25)
await websocket.send_json(event)
except TimeoutError:
await websocket.send_json(collab_events.ping_event())
except (WebSocketDisconnect, RuntimeError):
pass
finally:
collab_events.unsubscribe(queue)
@router.get("/external-orders/summary", response_model=WeiboCollabSummaryOut)
def external_orders_summary(
include_void: bool = Query(False),
archive_scope: str = Query("active"),
db: Session = Depends(get_db),
_current: User = Depends(require_permission("weibo:collab:view")),
):
return collab.summary(db, include_void=include_void, archive_scope=archive_scope)
@router.get("/external-orders/owners", response_model=list[WeiboCollabOwnerOut])
def external_orders_owners(
db: Session = Depends(get_db),
_current: User = Depends(require_permission("weibo:collab:view")),
):
rows = (
db.query(User.id, User.username)
.filter(User.is_active.is_(True))
.order_by(User.username)
.all()
)
return [
{"id": int(user_id), "username": str(username)} for user_id, username in rows
]
@router.get("/collab-shops", response_model=list[WeiboCollabShopOut])
def list_collab_shops(
db: Session = Depends(get_db),
_current: User = Depends(require_permission("weibo:collab:view")),
):
return [_shop_out(shop) for shop in collab.list_shops(db, active_only=False)]
@router.post("/collab-shops", response_model=WeiboCollabShopOut)
def create_collab_shop(
req: WeiboCollabShopCreate,
db: Session = Depends(get_db),
current: User = Depends(require_permission("weibo:collab:edit")),
):
try:
shop = collab.create_shop(db, current, name=req.name, remark=req.remark)
except collab.CollabError as exc:
raise _http_error(exc) from exc
_publish_change(current, scope="shops", action="shop.create")
return _shop_out(shop)
@router.put("/collab-shops/{shop_id}", response_model=WeiboCollabShopOut)
def update_collab_shop(
shop_id: int,
req: WeiboCollabShopUpdate,
db: Session = Depends(get_db),
current: User = Depends(require_permission("weibo:collab:edit")),
):
shop = db.query(WeiboCollabShop).filter(WeiboCollabShop.id == shop_id).first()
if shop is None:
raise HTTPException(status_code=404, detail="店铺不存在")
try:
collab.update_shop(db, current, shop, req.model_dump(exclude_unset=True))
except collab.CollabError as exc:
raise _http_error(exc) from exc
db.refresh(shop)
_publish_change(current, scope="shops", action="shop.update")
return _shop_out(shop)
@router.delete("/collab-shops/{shop_id}")
def delete_collab_shop(
shop_id: int,
db: Session = Depends(get_db),
current: User = Depends(require_permission("weibo:collab:manage")),
):
shop = db.query(WeiboCollabShop).filter(WeiboCollabShop.id == shop_id).first()
if shop is None:
raise HTTPException(status_code=404, detail="店铺不存在")
try:
collab.delete_shop(db, shop)
except collab.CollabError as exc:
raise _http_error(exc) from exc
_publish_change(current, scope="shops", action="shop.delete")
return {"success": True}
@router.get("/collab-goods", response_model=list[WeiboCollabGoodsOut])
def list_collab_goods(
db: Session = Depends(get_db),
_current: User = Depends(require_permission("weibo:collab:view")),
):
return [_goods_out(goods) for goods in collab.list_goods(db, active_only=False)]
@router.post("/collab-goods", response_model=WeiboCollabGoodsOut)
def create_collab_goods(
req: WeiboCollabGoodsCreate,
db: Session = Depends(get_db),
current: User = Depends(require_permission("weibo:collab:edit")),
):
try:
goods = collab.create_goods(
db,
current,
name=req.name,
price=req.price,
goods_id=req.goods_id,
goods_image=req.goods_image,
remark=req.remark,
)
except collab.CollabError as exc:
raise _http_error(exc) from exc
_publish_change(current, scope="gifts", action="goods.create")
return _goods_out(goods)
@router.put("/collab-goods/{goods_id}", response_model=WeiboCollabGoodsOut)
def update_collab_goods(
goods_id: int,
req: WeiboCollabGoodsUpdate,
db: Session = Depends(get_db),
current: User = Depends(require_permission("weibo:collab:edit")),
):
goods = db.query(WeiboCollabGoods).filter(WeiboCollabGoods.id == goods_id).first()
if goods is None:
raise HTTPException(status_code=404, detail="商品字典不存在")
try:
collab.update_goods(db, current, goods, req.model_dump(exclude_unset=True))
except collab.CollabError as exc:
raise _http_error(exc) from exc
db.refresh(goods)
_publish_change(current, scope="gifts", action="goods.update")
return _goods_out(goods)
@router.delete("/collab-goods/{goods_id}")
def delete_collab_goods(
goods_id: int,
db: Session = Depends(get_db),
current: User = Depends(require_permission("weibo:collab:manage")),
):
goods = db.query(WeiboCollabGoods).filter(WeiboCollabGoods.id == goods_id).first()
if goods is None:
raise HTTPException(status_code=404, detail="商品字典不存在")
try:
collab.delete_goods(db, goods)
except collab.CollabError as exc:
raise _http_error(exc) from exc
_publish_change(current, scope="gifts", action="goods.delete")
return {"success": True}
@router.post(
"/external-orders/accounts/lookup", response_model=WeiboCollabAccountLookupOut
)
def lookup_external_order_account(
req: WeiboCollabAccountLookupRequest,
db: Session = Depends(get_db),
_current: User = Depends(require_permission("weibo:collab:edit")),
):
try:
return collab.lookup_account(
db,
req.account_id,
with_goods=req.with_goods,
activity=WEIBO_DEFAULT_ACTIVITY,
)
except collab.CollabError as exc:
raise _http_error(exc) from exc
@router.get(
"/external-orders/accounts/options",
response_model=list[WeiboCollabAccountOptionOut],
)
def external_order_account_options(
db: Session = Depends(get_db),
_current: User = Depends(require_permission("weibo:collab:view")),
):
rows = (
db.query(WeiboAccount)
.filter(WeiboAccount.is_enabled.is_(True))
.order_by(WeiboAccount.id.desc())
.all()
)
return [
{
"id": row.id,
"uid": row.uid,
"nickname": row.nickname,
"game_role_name": row.game_role_name,
"weidou_balance": row.weidou_balance,
"ck_check_status": row.ck_check_status,
}
for row in rows
]
@router.get("/external-orders", response_model=WeiboCollabOrderListOut)
def list_external_orders(
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=200),
archive_scope: str = Query("active"),
status: str = Query(""),
shop: str = Query(""),
owner_id: int | None = Query(None),
goods_name: str = Query(""),
keyword: str = Query(""),
include_void: bool = Query(False),
db: Session = Depends(get_db),
_current: User = Depends(require_permission("weibo:collab:view")),
):
return collab.list_orders(
db,
page=page,
page_size=page_size,
archive_scope=archive_scope,
status=status,
shop=shop,
owner_id=owner_id,
goods_name=goods_name,
keyword=keyword,
include_void=include_void,
)
@router.post("/external-orders", response_model=WeiboCollabOrderOut)
def create_external_order(
req: WeiboCollabOrderCreate,
db: Session = Depends(get_db),
current: User = Depends(require_permission("weibo:collab:edit")),
):
try:
order = collab.create_order(
db,
current,
order_no=req.order_no,
shop=req.shop,
owner_id=req.owner_id,
account_id=req.account_id,
goods_id=req.goods_id,
goods_name=req.goods_name,
goods_image=req.goods_image,
required_weidou=req.required_weidou,
home_images=req.home_images,
remark=req.remark,
draft=req.draft,
require_verified=req.require_verified,
activity=WEIBO_DEFAULT_ACTIVITY,
)
except collab.CollabError as exc:
raise _http_error(exc) from exc
_publish_change(current, scope="orders", action="order.create")
return _one(db, order)
@router.post("/external-orders/verify/batch")
def verify_external_orders_batch(
ids: list[int],
db: Session = Depends(get_db),
current: User = Depends(require_permission("weibo:collab:edit")),
):
rows = collab.list_orders_by_ids(db, ids)
results: list[dict] = []
for order in rows:
try:
with db.begin_nested():
collab.verify_order_account(
db,
current,
order,
activity=WEIBO_DEFAULT_ACTIVITY,
commit=False,
)
results.append(
{
"id": order.id,
"order_no": order.order_no,
"success": True,
"message": "",
}
)
except Exception as exc: # noqa: BLE001
results.append(
{
"id": order.id,
"order_no": order.order_no,
"success": False,
"message": str(exc),
}
)
db.commit()
_publish_change(current, scope="orders", action="order.verify_batch")
return {"results": results, "success": all(item["success"] for item in results)}
@router.post("/external-orders/balances/check")
def check_external_orders_balance(
req: WeiboCollabBalanceCheckRequest,
db: Session = Depends(get_db),
current: User = Depends(require_permission("weibo:collab:edit")),
):
try:
result = collab.check_balance_groups(
db,
current,
collab.list_orders_by_ids(db, req.ids),
threshold=req.threshold,
activity=WEIBO_DEFAULT_ACTIVITY,
)
except collab.CollabError as exc:
raise _http_error(exc) from exc
_publish_change(current, scope="orders", action="order.balance_check")
return result
@router.post("/external-orders/exchange-records/check")
def check_external_orders_exchange_records(
req: WeiboCollabExchangeCheckRequest,
db: Session = Depends(get_db),
current: User = Depends(require_permission("weibo:collab:edit")),
):
try:
result = collab.check_exchange_records(
db,
current,
collab.list_orders_by_ids(db, req.ids),
activity=WEIBO_DEFAULT_ACTIVITY,
)
except collab.CollabError as exc:
raise _http_error(exc) from exc
_publish_change(current, scope="orders", action="order.exchange_check")
return result
@router.post("/external-orders/status/batch")
def set_external_orders_status_batch(
req: WeiboCollabStatusBatchRequest,
db: Session = Depends(get_db),
current: User = Depends(require_permission("weibo:collab:edit")),
):
try:
result = collab.set_status_batch(
db,
current,
collab.list_orders_by_ids(db, req.ids),
req.status,
reason=req.reason,
rollback=req.rollback,
)
except collab.CollabError as exc:
raise _http_error(exc) from exc
_publish_change(current, scope="orders", action="order.status_batch")
return result
@router.post("/external-orders/archive")
def archive_external_orders(
req: WeiboCollabArchiveRequest,
db: Session = Depends(get_db),
current: User = Depends(require_permission("weibo:collab:edit")),
):
try:
result = collab.archive_orders(db, current, req.ids, reason=req.reason)
except collab.CollabError as exc:
raise _http_error(exc) from exc
_publish_change(current, scope="orders", action="order.archive")
return result
@router.post("/external-orders/unarchive")
def unarchive_external_orders(
ids: list[int],
db: Session = Depends(get_db),
current: User = Depends(require_permission("weibo:collab:edit")),
):
try:
result = collab.unarchive_orders(db, current, ids)
except collab.CollabError as exc:
raise _http_error(exc) from exc
_publish_change(current, scope="orders", action="order.unarchive")
return result
@router.get("/external-orders/{order_id}", response_model=WeiboCollabOrderOut)
def get_external_order(
order_id: int,
db: Session = Depends(get_db),
_current: User = Depends(require_permission("weibo:collab:view")),
):
try:
return _one(db, collab.get_order(db, order_id))
except collab.CollabError as exc:
raise _http_error(exc) from exc
@router.put("/external-orders/{order_id}", response_model=WeiboCollabOrderOut)
def update_external_order(
order_id: int,
req: WeiboCollabOrderUpdate,
db: Session = Depends(get_db),
current: User = Depends(require_permission("weibo:collab:edit")),
):
try:
order = collab.get_order(db, order_id)
collab.update_order(
db,
current,
order,
req.model_dump(exclude_unset=True),
expected_version=req.expected_version,
)
except collab.CollabError as exc:
raise _http_error(exc) from exc
_publish_change(current, scope="orders", action="order.update")
return _one(db, order)
@router.post("/external-orders/{order_id}/account", response_model=WeiboCollabOrderOut)
def set_external_order_account(
order_id: int,
req: WeiboCollabAccountRequest,
db: Session = Depends(get_db),
current: User = Depends(require_permission("weibo:collab:edit")),
):
try:
order = collab.get_order(db, order_id)
collab.set_order_account(
db,
current,
order,
req.account_id,
expected_version=req.expected_version,
activity=WEIBO_DEFAULT_ACTIVITY,
)
except collab.CollabError as exc:
raise _http_error(exc) from exc
_publish_change(current, scope="orders", action="order.account")
return _one(db, order)
@router.post("/external-orders/{order_id}/verify", response_model=WeiboCollabOrderOut)
def verify_external_order(
order_id: int,
db: Session = Depends(get_db),
current: User = Depends(require_permission("weibo:collab:edit")),
):
try:
order = collab.get_order(db, order_id)
collab.verify_order_account(
db,
current,
order,
activity=WEIBO_DEFAULT_ACTIVITY,
)
except collab.CollabError as exc:
raise _http_error(exc) from exc
_publish_change(current, scope="orders", action="order.verify")
return _one(db, order)
@router.post("/external-orders/{order_id}/status", response_model=WeiboCollabOrderOut)
def set_external_order_status(
order_id: int,
req: WeiboCollabStatusRequest,
db: Session = Depends(get_db),
current: User = Depends(require_permission("weibo:collab:edit")),
):
try:
order = collab.get_order(db, order_id)
collab._assert_version(order, req.expected_version)
collab.set_status(
db,
current,
order,
req.status,
reason=req.reason,
rollback=req.rollback,
)
except collab.CollabError as exc:
raise _http_error(exc) from exc
_publish_change(current, scope="orders", action="order.status")
return _one(db, order)
@router.get(
"/external-orders/{order_id}/metrics",
response_model=list[WeiboCollabMetricOut],
)
def external_order_metrics(
order_id: int,
db: Session = Depends(get_db),
_current: User = Depends(require_permission("weibo:collab:view")),
):
return collab.order_metrics(db, order_id)
@router.get(
"/external-orders/{order_id}/timeline",
response_model=list[WeiboCollabTimelineOut],
)
def external_order_timeline(
order_id: int,
limit: int = Query(100, ge=1, le=500),
db: Session = Depends(get_db),
_current: User = Depends(require_permission("weibo:collab:view")),
):
return collab.order_timeline(db, order_id, limit=limit)
@router.delete("/external-orders/{order_id}")
def delete_external_order(
order_id: int,
db: Session = Depends(get_db),
current: User = Depends(require_permission("weibo:collab:manage")),
):
try:
order = collab.get_order(db, order_id)
except collab.CollabError as exc:
raise _http_error(exc) from exc
db.delete(order)
db.commit()
_publish_change(current, scope="orders", action="order.delete")
return {"success": True}