641 lines
20 KiB
Python
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}
|