"""微博外部单协作接口。""" 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}