feat(weibo): 增加外部单协作台账

This commit is contained in:
yml2213
2026-11-08 11:41:54 +08:00
parent a0afa430ec
commit 79cfd4a634
16 changed files with 5217 additions and 4 deletions
+2
View File
@@ -34,6 +34,7 @@ from .routers import (
proxy,
users,
weibo,
weibo_collab,
yyb,
)
from .schemas import AppInfo
@@ -167,6 +168,7 @@ app.include_router(proxy.router)
app.include_router(cookies.router)
app.include_router(huya.router)
app.include_router(weibo.router)
app.include_router(weibo_collab.router)
app.include_router(douyu.router)
app.include_router(douyu_collab.router)
app.include_router(yyb.router)
@@ -0,0 +1,251 @@
"""增加微博外部单协作台账。"""
from __future__ import annotations
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
revision: str = "20261108_0071"
down_revision: str | None = "20261108_0070"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def _create_shops() -> None:
op.create_table(
"weibo_collab_shops",
sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True),
sa.Column("name", sa.String(length=64), nullable=False),
sa.Column("sort_order", sa.Integer(), nullable=False, server_default="0"),
sa.Column(
"is_active", sa.Boolean(), nullable=False, server_default=sa.text("1")
),
sa.Column("remark", sa.String(length=255), nullable=False, server_default=""),
sa.Column("created_by", sa.Integer(), nullable=True),
sa.Column("updated_by", sa.Integer(), nullable=True),
sa.Column("created_at", sa.DateTime(), nullable=False),
sa.Column("updated_at", sa.DateTime(), nullable=False),
sa.ForeignKeyConstraint(["created_by"], ["users.id"]),
sa.ForeignKeyConstraint(["updated_by"], ["users.id"]),
)
op.create_index(
"ix_weibo_collab_shops_name", "weibo_collab_shops", ["name"], unique=True
)
op.create_index(
"ix_weibo_collab_shops_is_active", "weibo_collab_shops", ["is_active"]
)
def _create_goods() -> None:
op.create_table(
"weibo_collab_goods",
sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True),
sa.Column("name", sa.String(length=128), nullable=False),
sa.Column("price", sa.Integer(), nullable=False, server_default="0"),
sa.Column("goods_id", sa.Integer(), nullable=True),
sa.Column(
"goods_image", sa.String(length=512), nullable=False, server_default=""
),
sa.Column("sort_order", sa.Integer(), nullable=False, server_default="0"),
sa.Column(
"is_active", sa.Boolean(), nullable=False, server_default=sa.text("1")
),
sa.Column("remark", sa.String(length=255), nullable=False, server_default=""),
sa.Column("created_by", sa.Integer(), nullable=True),
sa.Column("updated_by", sa.Integer(), nullable=True),
sa.Column("created_at", sa.DateTime(), nullable=False),
sa.Column("updated_at", sa.DateTime(), nullable=False),
sa.ForeignKeyConstraint(["created_by"], ["users.id"]),
sa.ForeignKeyConstraint(["updated_by"], ["users.id"]),
)
op.create_index(
"ix_weibo_collab_goods_name", "weibo_collab_goods", ["name"], unique=True
)
op.create_index(
"ix_weibo_collab_goods_is_active", "weibo_collab_goods", ["is_active"]
)
def _create_orders() -> None:
op.create_table(
"weibo_external_orders",
sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True),
sa.Column("order_no", sa.String(length=64), nullable=False),
sa.Column("shop", sa.String(length=64), nullable=False, server_default=""),
sa.Column("owner_id", sa.Integer(), nullable=True),
sa.Column("account_id", sa.Integer(), nullable=True),
sa.Column(
"account_uid", sa.String(length=32), nullable=False, server_default=""
),
sa.Column(
"account_nickname",
sa.String(length=128),
nullable=False,
server_default="",
),
sa.Column(
"game_role_name", sa.String(length=128), nullable=False, server_default=""
),
sa.Column("bind_snapshot_at", sa.DateTime(), nullable=True),
sa.Column("goods_id", sa.Integer(), nullable=True),
sa.Column(
"goods_name", sa.String(length=128), nullable=False, server_default=""
),
sa.Column(
"goods_image", sa.String(length=512), nullable=False, server_default=""
),
sa.Column("required_weidou", sa.Integer(), nullable=True),
sa.Column("home_images", sa.JSON(), nullable=True),
sa.Column("remark", sa.String(length=512), nullable=False, server_default=""),
sa.Column(
"status",
sa.String(length=20),
nullable=False,
server_default="incomplete",
),
sa.Column("recharged_at", sa.DateTime(), nullable=True),
sa.Column("exchange_started_at", sa.DateTime(), nullable=True),
sa.Column("done_at", sa.DateTime(), nullable=True),
sa.Column(
"void_reason", sa.String(length=256), nullable=False, server_default=""
),
sa.Column("weidou_balance", sa.Integer(), nullable=True),
sa.Column("weidou_required_total", sa.Integer(), nullable=True),
sa.Column("weidou_surplus", sa.Integer(), nullable=True),
sa.Column("weidou_threshold", sa.Integer(), nullable=True),
sa.Column("weidou_checked_at", sa.DateTime(), nullable=True),
sa.Column(
"weidou_check_reason",
sa.String(length=16),
nullable=False,
server_default="",
),
sa.Column(
"weidou_check_message",
sa.String(length=512),
nullable=False,
server_default="",
),
sa.Column("weidou_check_detail", sa.JSON(), nullable=True),
sa.Column("exchange_checked_at", sa.DateTime(), nullable=True),
sa.Column(
"exchange_record_key",
sa.String(length=128),
nullable=False,
server_default="",
),
sa.Column("exchange_record_snapshot", sa.JSON(), nullable=True),
sa.Column(
"exchange_verdict",
sa.String(length=16),
nullable=False,
server_default="",
),
sa.Column(
"exchange_message",
sa.String(length=512),
nullable=False,
server_default="",
),
sa.Column("archived_at", sa.DateTime(), nullable=True),
sa.Column("archived_by", sa.Integer(), nullable=True),
sa.Column(
"archive_reason", sa.String(length=256), nullable=False, server_default=""
),
sa.Column("created_by", sa.Integer(), nullable=True),
sa.Column("updated_by", sa.Integer(), nullable=True),
sa.Column("version", sa.Integer(), nullable=False, server_default="0"),
sa.Column("created_at", sa.DateTime(), nullable=False),
sa.Column("updated_at", sa.DateTime(), nullable=False),
sa.ForeignKeyConstraint(
["account_id"], ["weibo_accounts.id"], ondelete="SET NULL"
),
sa.ForeignKeyConstraint(["owner_id"], ["users.id"]),
sa.ForeignKeyConstraint(["archived_by"], ["users.id"]),
sa.ForeignKeyConstraint(["created_by"], ["users.id"]),
sa.ForeignKeyConstraint(["updated_by"], ["users.id"]),
)
for column in (
"order_no",
"shop",
"owner_id",
"account_id",
"account_uid",
"status",
"exchange_verdict",
"archived_at",
"created_by",
"updated_at",
):
op.create_index(
f"ix_weibo_external_orders_{column}",
"weibo_external_orders",
[column],
unique=column == "order_no",
)
def _create_metrics() -> None:
op.create_table(
"weibo_external_order_metrics",
sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True),
sa.Column("order_id", sa.Integer(), nullable=False),
sa.Column("order_no", sa.String(length=64), nullable=False, server_default=""),
sa.Column("account_id", sa.Integer(), nullable=True),
sa.Column(
"account_uid", sa.String(length=32), nullable=False, server_default=""
),
sa.Column("weidou_balance", sa.Integer(), nullable=True),
sa.Column("weidou_required_total", sa.Integer(), nullable=True),
sa.Column("weidou_surplus", sa.Integer(), nullable=True),
sa.Column("records_total", sa.Integer(), nullable=False, server_default="0"),
sa.Column("records_matched", sa.Integer(), nullable=False, server_default="0"),
sa.Column(
"verdict", sa.String(length=16), nullable=False, server_default="unknown"
),
sa.Column("message", sa.String(length=512), nullable=False, server_default=""),
sa.Column(
"source", sa.String(length=24), nullable=False, server_default="manual"
),
sa.Column("raw", sa.JSON(), nullable=True),
sa.Column("created_by", sa.Integer(), nullable=True),
sa.Column("created_at", sa.DateTime(), nullable=False),
sa.ForeignKeyConstraint(
["order_id"], ["weibo_external_orders.id"], ondelete="CASCADE"
),
sa.ForeignKeyConstraint(["created_by"], ["users.id"]),
)
for column in ("order_id", "account_id", "verdict", "created_at"):
op.create_index(
f"ix_weibo_external_order_metrics_{column}",
"weibo_external_order_metrics",
[column],
)
def upgrade() -> None:
bind = op.get_bind()
existing = set(sa.inspect(bind).get_table_names())
if "weibo_collab_shops" not in existing:
_create_shops()
if "weibo_collab_goods" not in existing:
_create_goods()
if "weibo_external_orders" not in existing:
_create_orders()
if "weibo_external_order_metrics" not in existing:
_create_metrics()
def downgrade() -> None:
bind = op.get_bind()
existing = set(sa.inspect(bind).get_table_names())
if "weibo_external_order_metrics" in existing:
op.drop_table("weibo_external_order_metrics")
if "weibo_external_orders" in existing:
op.drop_table("weibo_external_orders")
if "weibo_collab_goods" in existing:
op.drop_table("weibo_collab_goods")
if "weibo_collab_shops" in existing:
op.drop_table("weibo_collab_shops")
+193
View File
@@ -961,6 +961,199 @@ class WeiboAccount(Base):
)
class WeiboCollabShop(Base):
"""微博外部单协作的店铺字典。"""
__tablename__ = "weibo_collab_shops"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
name: Mapped[str] = mapped_column(
String(64), unique=True, nullable=False, index=True
)
sort_order: Mapped[int] = mapped_column(Integer, default=0, server_default="0")
is_active: Mapped[bool] = mapped_column(
Boolean, default=True, server_default="1", index=True
)
remark: Mapped[str] = mapped_column(String(255), default="", server_default="")
created_by: Mapped[int | None] = mapped_column(
ForeignKey("users.id"), nullable=True
)
updated_by: Mapped[int | None] = mapped_column(
ForeignKey("users.id"), nullable=True
)
created_at: Mapped[datetime] = mapped_column(UTCDateTime, default=_utcnow)
updated_at: Mapped[datetime] = mapped_column(
UTCDateTime, default=_utcnow, onupdate=_utcnow
)
class WeiboCollabGoods(Base):
"""微博外部单协作的商品字典,微豆价格仅作下单快照兜底。"""
__tablename__ = "weibo_collab_goods"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
name: Mapped[str] = mapped_column(
String(128), unique=True, nullable=False, index=True
)
price: Mapped[int] = mapped_column(
Integer, default=0, server_default="0", nullable=False
)
goods_id: Mapped[int | None] = mapped_column(Integer, nullable=True)
goods_image: Mapped[str] = mapped_column(String(512), default="", server_default="")
sort_order: Mapped[int] = mapped_column(Integer, default=0, server_default="0")
is_active: Mapped[bool] = mapped_column(
Boolean, default=True, server_default="1", index=True
)
remark: Mapped[str] = mapped_column(String(255), default="", server_default="")
created_by: Mapped[int | None] = mapped_column(
ForeignKey("users.id"), nullable=True
)
updated_by: Mapped[int | None] = mapped_column(
ForeignKey("users.id"), nullable=True
)
created_at: Mapped[datetime] = mapped_column(UTCDateTime, default=_utcnow)
updated_at: Mapped[datetime] = mapped_column(
UTCDateTime, default=_utcnow, onupdate=_utcnow
)
class WeiboExternalOrder(Base):
"""微博外部单协作台账:一行 = 一单,引用现有微博账号池。"""
__tablename__ = "weibo_external_orders"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
order_no: Mapped[str] = mapped_column(
String(64), unique=True, nullable=False, index=True
)
shop: Mapped[str] = mapped_column(
String(64), default="", server_default="", index=True
)
owner_id: Mapped[int | None] = mapped_column(
ForeignKey("users.id"), nullable=True, index=True
)
account_id: Mapped[int | None] = mapped_column(
ForeignKey("weibo_accounts.id", ondelete="SET NULL"),
nullable=True,
index=True,
)
account_uid: Mapped[str] = mapped_column(
String(32), default="", server_default="", index=True
)
account_nickname: Mapped[str] = mapped_column(
String(128), default="", server_default=""
)
game_role_name: Mapped[str] = mapped_column(
String(128), default="", server_default=""
)
bind_snapshot_at: Mapped[datetime | None] = mapped_column(
UTCDateTime, nullable=True
)
goods_id: Mapped[int | None] = mapped_column(Integer, nullable=True)
goods_name: Mapped[str] = mapped_column(String(128), default="", server_default="")
goods_image: Mapped[str] = mapped_column(String(512), default="", server_default="")
required_weidou: Mapped[int | None] = mapped_column(Integer, nullable=True)
home_images: Mapped[list | None] = mapped_column(JSON, nullable=True)
remark: Mapped[str] = mapped_column(String(512), default="", server_default="")
status: Mapped[str] = mapped_column(
String(20), default="incomplete", server_default="incomplete", index=True
)
recharged_at: Mapped[datetime | None] = mapped_column(UTCDateTime, nullable=True)
exchange_started_at: Mapped[datetime | None] = mapped_column(
UTCDateTime, nullable=True
)
done_at: Mapped[datetime | None] = mapped_column(UTCDateTime, nullable=True)
void_reason: Mapped[str] = mapped_column(String(256), default="", server_default="")
weidou_balance: Mapped[int | None] = mapped_column(Integer, nullable=True)
weidou_required_total: Mapped[int | None] = mapped_column(Integer, nullable=True)
weidou_surplus: Mapped[int | None] = mapped_column(Integer, nullable=True)
weidou_threshold: Mapped[int | None] = mapped_column(Integer, nullable=True)
weidou_checked_at: Mapped[datetime | None] = mapped_column(
UTCDateTime, nullable=True
)
weidou_check_reason: Mapped[str] = mapped_column(
String(16), default="", server_default=""
)
weidou_check_message: Mapped[str] = mapped_column(
String(512), default="", server_default=""
)
weidou_check_detail: Mapped[dict | None] = mapped_column(JSON, nullable=True)
exchange_checked_at: Mapped[datetime | None] = mapped_column(
UTCDateTime, nullable=True
)
exchange_record_key: Mapped[str] = mapped_column(
String(128), default="", server_default=""
)
exchange_record_snapshot: Mapped[dict | None] = mapped_column(JSON, nullable=True)
exchange_verdict: Mapped[str] = mapped_column(
String(16), default="", server_default="", index=True
)
exchange_message: Mapped[str] = mapped_column(
String(512), default="", server_default=""
)
archived_at: Mapped[datetime | None] = mapped_column(
UTCDateTime, nullable=True, index=True
)
archived_by: Mapped[int | None] = mapped_column(
ForeignKey("users.id"), nullable=True
)
archive_reason: Mapped[str] = mapped_column(
String(256), default="", server_default=""
)
created_by: Mapped[int | None] = mapped_column(
ForeignKey("users.id"), nullable=True, index=True
)
updated_by: Mapped[int | None] = mapped_column(
ForeignKey("users.id"), nullable=True
)
version: Mapped[int] = mapped_column(Integer, default=0, server_default="0")
created_at: Mapped[datetime] = mapped_column(UTCDateTime, default=_utcnow)
updated_at: Mapped[datetime] = mapped_column(
UTCDateTime, default=_utcnow, onupdate=_utcnow, index=True
)
class WeiboExternalOrderMetric(Base):
"""微博协作单的核账与兑换记录快照,只追加,不保存 CK。"""
__tablename__ = "weibo_external_order_metrics"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
order_id: Mapped[int] = mapped_column(
ForeignKey("weibo_external_orders.id", ondelete="CASCADE"), index=True
)
order_no: Mapped[str] = mapped_column(String(64), default="", server_default="")
account_id: Mapped[int | None] = mapped_column(Integer, nullable=True, index=True)
account_uid: Mapped[str] = mapped_column(String(32), default="", server_default="")
weidou_balance: Mapped[int | None] = mapped_column(Integer, nullable=True)
weidou_required_total: Mapped[int | None] = mapped_column(Integer, nullable=True)
weidou_surplus: Mapped[int | None] = mapped_column(Integer, nullable=True)
records_total: Mapped[int] = mapped_column(Integer, default=0, server_default="0")
records_matched: Mapped[int] = mapped_column(Integer, default=0, server_default="0")
verdict: Mapped[str] = mapped_column(
String(16), default="unknown", server_default="unknown", index=True
)
message: Mapped[str] = mapped_column(String(512), default="", server_default="")
source: Mapped[str] = mapped_column(
String(24), default="manual", server_default="manual"
)
raw: Mapped[dict | None] = mapped_column(JSON, nullable=True)
created_by: Mapped[int | None] = mapped_column(
ForeignKey("users.id"), nullable=True
)
created_at: Mapped[datetime] = mapped_column(
UTCDateTime, default=_utcnow, index=True
)
class HuyaRegisterBatch(Base):
"""虎牙自动注册批次(持久化元数据)"""
+8
View File
@@ -49,6 +49,9 @@ PERMISSIONS = {
"weibo:recharge": "创建微博助威充值订单",
"weibo:exchange": "执行微博微豆兑换",
"weibo:delete": "删除微博账号",
"weibo:collab:view": "查看微博外部单协作",
"weibo:collab:edit": "编辑微博外部单协作",
"weibo:collab:manage": "删除微博外部单协作及字典",
# 虎牙
"huya:account": "虎牙账号管理(兼容旧权限)",
"huya:view_all": "查看所有虎牙账号",
@@ -116,6 +119,9 @@ ROLE_PERMISSIONS = {
"weibo:recharge",
"weibo:exchange",
"weibo:delete",
"weibo:collab:view",
"weibo:collab:edit",
"weibo:collab:manage",
"huya:account",
"huya:view_all",
"huya:import",
@@ -131,6 +137,8 @@ ROLE_PERMISSIONS = {
# 协作台账是共享表:运营既能改也能删。
"huya:collab:view",
"huya:collab:edit",
"weibo:collab:view",
"weibo:collab:edit",
"huya:collab:manage",
"huya:collab:ck_export",
"proxy:manage",
+640
View File
@@ -0,0 +1,640 @@
"""微博外部单协作接口。"""
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}
+265
View File
@@ -629,6 +629,271 @@ class WeiboPaymentStatusOut(BaseModel):
sub_status: str = ""
# ==================== 微博外部单协作 ====================
class WeiboCollabShopCreate(BaseModel):
name: str = Field(..., min_length=1, max_length=64)
remark: str = Field("", max_length=255)
class WeiboCollabShopUpdate(BaseModel):
name: str | None = Field(None, min_length=1, max_length=64)
sort_order: int | None = None
is_active: bool | None = None
remark: str | None = Field(None, max_length=255)
class WeiboCollabShopOut(BaseModel):
id: int
name: str
sort_order: int = 0
is_active: bool = True
remark: str = ""
created_by: int | None = None
updated_by: int | None = None
created_at: datetime | None = None
updated_at: datetime | None = None
class WeiboCollabGoodsCreate(BaseModel):
name: str = Field(..., min_length=1, max_length=128)
price: int = Field(..., ge=0)
goods_id: int | None = Field(None, gt=0)
goods_image: str = Field("", max_length=512)
remark: str = Field("", max_length=255)
class WeiboCollabGoodsUpdate(BaseModel):
name: str | None = Field(None, min_length=1, max_length=128)
price: int | None = Field(None, ge=0)
goods_id: int | None = Field(None, gt=0)
goods_image: str | None = Field(None, max_length=512)
sort_order: int | None = None
is_active: bool | None = None
remark: str | None = Field(None, max_length=255)
class WeiboCollabGoodsOut(BaseModel):
id: int
name: str
price: int = 0
goods_id: int | None = None
goods_image: str = ""
sort_order: int = 0
is_active: bool = True
remark: str = ""
created_by: int | None = None
updated_by: int | None = None
created_at: datetime | None = None
updated_at: datetime | None = None
class WeiboCollabLiveGoodsOut(BaseModel):
id: int
goods_name: str = ""
goods_image: str = ""
price: int = 0
remain_count: int | None = None
is_sold_out: bool = False
class WeiboCollabAccountLookupRequest(BaseModel):
account_id: int = Field(..., gt=0)
with_goods: bool = True
class WeiboCollabAccountOptionOut(BaseModel):
id: int
uid: str = ""
nickname: str = ""
game_role_name: str = ""
weidou_balance: int | None = None
ck_check_status: str = ""
class WeiboCollabAccountLookupOut(BaseModel):
ok: bool = False
message: str = ""
account_id: int
uid: str = ""
nickname: str = ""
is_enabled: bool = True
ck_check_status: str = ""
ck_check_message: str = ""
is_bound: bool | None = None
game_role_name: str = ""
weidou_balance: int | None = None
goods: list[WeiboCollabLiveGoodsOut] = Field(default_factory=list)
orders_using_account: list[dict[str, Any]] = Field(default_factory=list)
class WeiboCollabOrderCreate(BaseModel):
order_no: str = Field(..., min_length=1, max_length=64)
shop: str = Field("", max_length=64)
owner_id: int | None = None
account_id: int | None = None
goods_id: int | None = Field(None, gt=0)
goods_name: str = Field("", max_length=128)
goods_image: str = Field("", max_length=512)
required_weidou: int | None = Field(None, ge=0)
home_images: list[str] = Field(default_factory=list)
remark: str = Field("", max_length=512)
draft: bool = False
require_verified: bool = True
class WeiboCollabOrderUpdate(BaseModel):
order_no: str | None = Field(None, max_length=64)
shop: str | None = Field(None, max_length=64)
owner_id: int | None = None
goods_id: int | None = Field(None, gt=0)
goods_name: str | None = Field(None, max_length=128)
goods_image: str | None = Field(None, max_length=512)
required_weidou: int | None = Field(None, ge=0)
home_images: list[str] | None = None
remark: str | None = Field(None, max_length=512)
expected_version: int | None = None
class WeiboCollabAccountRequest(BaseModel):
account_id: int = Field(..., gt=0)
expected_version: int | None = None
class WeiboCollabStatusRequest(BaseModel):
status: str = Field(..., min_length=1, max_length=20)
reason: str = Field("", max_length=256)
rollback: bool = False
expected_version: int | None = None
class WeiboCollabStatusBatchRequest(BaseModel):
ids: list[int] = Field(default_factory=list)
status: str = Field(..., min_length=1, max_length=20)
reason: str = Field("", max_length=256)
rollback: bool = False
class WeiboCollabBalanceCheckRequest(BaseModel):
ids: list[int] = Field(default_factory=list)
threshold: int = Field(100, ge=0, le=1_000_000)
class WeiboCollabExchangeCheckRequest(BaseModel):
ids: list[int] = Field(default_factory=list)
class WeiboCollabArchiveRequest(BaseModel):
ids: list[int] = Field(default_factory=list)
reason: str = Field("", max_length=256)
class WeiboCollabOrderOut(BaseModel):
id: int
order_no: str
shop: str = ""
owner_id: int | None = None
owner_name: str = ""
account_id: int | None = None
account_uid: str = ""
account_nickname: str = ""
account_exists: bool = False
account_enabled: bool = False
account_ck_status: str = ""
account_ck_message: str = ""
account_is_bound: bool | None = None
game_role_name: str = ""
bind_snapshot_at: datetime | None = None
goods_id: int | None = None
goods_name: str = ""
goods_image: str = ""
required_weidou: int | None = None
home_images: list[str] = Field(default_factory=list)
remark: str = ""
status: str = "incomplete"
status_label: str = ""
next_status: str | None = None
missing_fields: list[str] = Field(default_factory=list)
weidou_balance: int | None = None
weidou_required_total: int | None = None
weidou_surplus: int | None = None
weidou_threshold: int | None = None
weidou_checked_at: datetime | None = None
weidou_check_reason: str = ""
weidou_check_message: str = ""
weidou_check_detail: dict[str, Any] | None = None
exchange_started_at: datetime | None = None
exchange_checked_at: datetime | None = None
exchange_record_key: str = ""
exchange_record_snapshot: dict[str, Any] | None = None
exchange_verdict: str = ""
exchange_message: str = ""
recharged_at: datetime | None = None
done_at: datetime | None = None
void_reason: str = ""
is_archived: bool = False
archived_at: datetime | None = None
archived_by: int | None = None
archived_by_name: str = ""
archive_reason: str = ""
created_by: int | None = None
created_by_name: str = ""
updated_by: int | None = None
updated_by_name: str = ""
version: int = 0
created_at: datetime | None = None
updated_at: datetime | None = None
account_cluster: dict[str, Any] | None = None
last_metric_at: datetime | None = None
last_metric_message: str = ""
class WeiboCollabOrderListOut(BaseModel):
items: list[WeiboCollabOrderOut]
total: int
page: int
page_size: int
class WeiboCollabSummaryOut(BaseModel):
total: int
by_status: dict[str, int]
account_missing: int = 0
account_disabled: int = 0
ck_invalid: int = 0
archived_total: int = 0
class WeiboCollabOwnerOut(BaseModel):
id: int
username: str
class WeiboCollabMetricOut(BaseModel):
id: int
order_id: int
order_no: str = ""
account_uid: str = ""
weidou_balance: int | None = None
weidou_required_total: int | None = None
weidou_surplus: int | None = None
records_total: int = 0
records_matched: int = 0
verdict: str = ""
verdict_label: str = ""
message: str = ""
source: str = ""
created_at: datetime | None = None
class WeiboCollabTimelineOut(BaseModel):
id: int
action: str
username: str = ""
detail: str = ""
success: bool | None = None
created_at: datetime | None = None
# ---- 虎牙 ----
class HuyaCookieImport(BaseModel):
"""批量导入虎牙 Cookie。支持纯 CK 或 账号----密码----CK。"""
@@ -0,0 +1,82 @@
"""微博外部单协作的进程内实时失效通知。"""
from __future__ import annotations
import asyncio
import itertools
import threading
from datetime import UTC, datetime
from typing import Any
class CollabEventHub:
def __init__(self) -> None:
self._lock = threading.Lock()
self._revisions = itertools.count(1)
self._current_revision = 0
self._loop: asyncio.AbstractEventLoop | None = None
self._subscribers: set[asyncio.Queue[dict[str, Any]]] = set()
def subscribe(self) -> asyncio.Queue[dict[str, Any]]:
loop = asyncio.get_running_loop()
queue: asyncio.Queue[dict[str, Any]] = asyncio.Queue(maxsize=100)
with self._lock:
self._loop = loop
self._subscribers.add(queue)
return queue
def unsubscribe(self, queue: asyncio.Queue[dict[str, Any]]) -> None:
with self._lock:
self._subscribers.discard(queue)
def ready_event(self) -> dict[str, Any]:
with self._lock:
return {"type": "collab.ready", "revision": self._current_revision}
def ping_event(self) -> dict[str, Any]:
with self._lock:
return {"type": "collab.ping", "revision": self._current_revision}
def publish(
self,
*,
scope: str,
action: str,
actor_id: int | None = None,
) -> dict[str, Any]:
with self._lock:
revision = next(self._revisions)
self._current_revision = revision
loop = self._loop
subscribers = list(self._subscribers)
event = {
"type": "collab.invalidate",
"revision": revision,
"scope": scope,
"action": action,
"actor_id": actor_id,
"at": datetime.now(UTC).isoformat(),
}
if loop is None:
return event
for queue in subscribers:
try:
loop.call_soon_threadsafe(self._offer, queue, event)
except RuntimeError:
continue
return event
@staticmethod
def _offer(queue: asyncio.Queue[dict[str, Any]], event: dict[str, Any]) -> None:
while queue.full():
try:
queue.get_nowait()
except asyncio.QueueEmpty:
break
try:
queue.put_nowait(event)
except asyncio.QueueFull:
pass
collab_events = CollabEventHub()
File diff suppressed because it is too large Load Diff
+6 -3
View File
@@ -115,6 +115,7 @@ def refresh_weibo_account(
account: WeiboAccount,
*,
activity: str = "",
commit: bool = True,
) -> WeiboHomeSnapshot:
"""刷新一个微博账号;失败时保留旧资料并记录本次检查状态。"""
@@ -126,12 +127,14 @@ def refresh_weibo_account(
account.ck_check_status = _error_status(exc)
account.ck_check_message = str(exc)
account.ck_checked_at = _now()
db.commit()
if commit:
db.commit()
raise
_apply_snapshot(account, snapshot, account.cookie or "")
db.commit()
db.refresh(account)
if commit:
db.commit()
db.refresh(account)
return snapshot
+2
View File
@@ -32,6 +32,7 @@ const HuyaElitePage = lazy(() => import('./pages/HuyaElitePage'));
const HuyaCollabPage = lazy(() => import('./pages/HuyaCollabPage'));
const HuyaBrowserExchangePage = lazy(() => import('./pages/HuyaBrowserExchangePage'));
const WeiboAccountsPage = lazy(() => import('./pages/WeiboAccountsPage'));
const WeiboCollabPage = lazy(() => import('./pages/WeiboCollabPage'));
const YybRechargePage = lazy(() => import('./pages/YybRechargePage'));
const AuditLogsPage = lazy(() => import('./pages/AuditLogsPage'));
@@ -107,6 +108,7 @@ function AppContent() {
<Route path="huya/collab" element={lazyRoute(<HuyaCollabPage />)} />
<Route path="huya/browser-exchange" element={lazyRoute(<HuyaBrowserExchangePage />)} />
<Route path="weibo/accounts" element={lazyRoute(<WeiboAccountsPage />)} />
<Route path="weibo/collab" element={lazyRoute(<WeiboCollabPage />)} />
<Route path="yyb/recharge" element={lazyRoute(<YybRechargePage />)} />
<Route path="proxy" element={lazyRoute(<ProxyPage />)} />
<Route path="automation" element={lazyRoute(<AutomationPage />)} />
+24
View File
@@ -15,4 +15,28 @@ export { loginApi } from './login';
export { proxyApi } from './proxy';
export { userApi } from './users';
export { weiboApi } from './weibo';
export { weiboCollabApi } from './weiboCollab';
export type {
WeiboCollabAccountLookup,
WeiboCollabAccountOption,
WeiboCollabArchiveResult,
WeiboCollabArchiveScope,
WeiboCollabBalanceCheckResult,
WeiboCollabExchangeCheckResult,
WeiboCollabGoods,
WeiboCollabLiveGoods,
WeiboCollabMetric,
WeiboCollabOrder,
WeiboCollabOrderCreate,
WeiboCollabOrderList,
WeiboCollabOrderUpdate,
WeiboCollabOwner,
WeiboCollabShop,
WeiboCollabStatus,
WeiboCollabSummary,
WeiboCollabTimeline,
WeiboCollabUnarchiveResult,
WeiboCollabVerdict,
WeiboCollabVerifyBatchResult,
} from './weiboCollab';
export { yybApi } from './yyb';
+408
View File
@@ -0,0 +1,408 @@
import api from './client';
export type WeiboCollabStatus =
| 'incomplete'
| 'to_recharge'
| 'recharged'
| 'exchange_pending'
| 'done'
| 'weidou_error'
| 'failed'
| 'void';
export type WeiboCollabArchiveScope = 'active' | 'archived' | 'all';
export type WeiboCollabVerdict = 'success' | 'failed' | 'unknown' | '';
export interface WeiboCollabShop {
id: number;
name: string;
sort_order: number;
is_active: boolean;
remark: string;
created_by: number | null;
updated_by: number | null;
created_at: string | null;
updated_at: string | null;
}
export interface WeiboCollabGoods {
id: number;
name: string;
price: number;
goods_id: number | null;
goods_image: string;
sort_order: number;
is_active: boolean;
remark: string;
created_by: number | null;
updated_by: number | null;
created_at: string | null;
updated_at: string | null;
}
export interface WeiboCollabLiveGoods {
id: number;
goods_name: string;
goods_image: string;
price: number;
remain_count: number | null;
is_sold_out: boolean;
}
export interface WeiboCollabAccountLookup {
ok: boolean;
message: string;
account_id: number;
uid: string;
nickname: string;
is_enabled: boolean;
ck_check_status: string;
ck_check_message: string;
is_bound: boolean | null;
game_role_name: string;
weidou_balance: number | null;
goods: WeiboCollabLiveGoods[];
orders_using_account: Array<{
id: number;
order_no: string;
status: WeiboCollabStatus;
status_label: string;
}>;
}
export interface WeiboCollabAccountOption {
id: number;
uid: string;
nickname: string;
game_role_name: string;
weidou_balance: number | null;
ck_check_status: string;
}
export interface WeiboCollabOrder {
id: number;
order_no: string;
shop: string;
owner_id: number | null;
owner_name: string;
account_id: number | null;
account_uid: string;
account_nickname: string;
account_exists: boolean;
account_enabled: boolean;
account_ck_status: string;
account_ck_message: string;
account_is_bound: boolean | null;
game_role_name: string;
bind_snapshot_at: string | null;
goods_id: number | null;
goods_name: string;
goods_image: string;
required_weidou: number | null;
home_images: string[];
remark: string;
status: WeiboCollabStatus;
status_label: string;
next_status: WeiboCollabStatus | null;
missing_fields: string[];
weidou_balance: number | null;
weidou_required_total: number | null;
weidou_surplus: number | null;
weidou_threshold: number | null;
weidou_checked_at: string | null;
weidou_check_reason: string;
weidou_check_message: string;
weidou_check_detail: Record<string, unknown> | null;
exchange_started_at: string | null;
exchange_checked_at: string | null;
exchange_record_key: string;
exchange_record_snapshot: Record<string, unknown> | null;
exchange_verdict: WeiboCollabVerdict;
exchange_message: string;
recharged_at: string | null;
done_at: string | null;
void_reason: string;
is_archived: boolean;
archived_at: string | null;
archived_by: number | null;
archived_by_name: string;
archive_reason: string;
created_by: number | null;
created_by_name: string;
updated_by: number | null;
updated_by_name: string;
version: number;
created_at: string | null;
updated_at: string | null;
account_cluster: {
total: number;
orders: Array<{
id: number;
order_no: string;
status: WeiboCollabStatus;
status_label: string;
goods_name: string;
}>;
} | null;
last_metric_at: string | null;
last_metric_message: string;
}
export interface WeiboCollabOrderList {
items: WeiboCollabOrder[];
total: number;
page: number;
page_size: number;
}
export interface WeiboCollabSummary {
total: number;
by_status: Record<string, number>;
account_missing: number;
account_disabled: number;
ck_invalid: number;
archived_total: number;
}
export interface WeiboCollabOwner {
id: number;
username: string;
}
export interface WeiboCollabMetric {
id: number;
order_id: number;
order_no: string;
account_uid: string;
weidou_balance: number | null;
weidou_required_total: number | null;
weidou_surplus: number | null;
records_total: number;
records_matched: number;
verdict: WeiboCollabVerdict;
verdict_label: string;
message: string;
source: string;
created_at: string | null;
}
export interface WeiboCollabTimeline {
id: number;
action: string;
username: string;
detail: string;
success: boolean | null;
created_at: string | null;
}
export interface WeiboCollabOrderCreate {
order_no: string;
shop?: string;
owner_id?: number | null;
account_id?: number | null;
goods_id?: number | null;
goods_name?: string;
goods_image?: string;
required_weidou?: number | null;
home_images?: string[];
remark?: string;
draft?: boolean;
require_verified?: boolean;
}
export interface WeiboCollabOrderUpdate {
order_no?: string;
shop?: string;
owner_id?: number | null;
goods_id?: number | null;
goods_name?: string;
goods_image?: string;
required_weidou?: number | null;
home_images?: string[];
remark?: string;
expected_version?: number;
}
export interface WeiboCollabBalanceCheckResult {
total: number;
updated: number;
threshold: number;
groups: Array<{
account_id: number;
uid: string;
status: string;
status_label: string;
balance: number | null;
required_total: number;
surplus: number | null;
threshold: number;
reason: string;
message: string;
order_ids: number[];
}>;
failures: Array<{ id: number; order_no: string; message: string }>;
skipped: Array<{ id: number; order_no: string; message: string }>;
}
export interface WeiboCollabExchangeCheckResult {
total: number;
success: number;
failed: number;
unknown: number;
results: Array<{
id: number;
order_no: string;
ok: boolean;
verdict: WeiboCollabVerdict;
verdict_label: string;
message: string;
records_total: number;
records_matched: number;
status: WeiboCollabStatus;
status_label: string;
metric_id: number;
}>;
failures: Array<{ id: number; order_no: string; message: string }>;
}
export interface WeiboCollabVerifyBatchResult {
success: boolean;
results: Array<{
id: number;
order_no: string;
success: boolean;
message: string;
}>;
}
export interface WeiboCollabArchiveResult {
total: number;
archived: number;
skipped: number;
results: Array<{ id: number; order_no: string; ok: boolean; message: string }>;
}
export interface WeiboCollabUnarchiveResult {
total: number;
unarchived: number;
skipped: number;
results: Array<{ id: number; order_no: string; ok: boolean; message: string }>;
}
export const weiboCollabApi = {
accounts: () =>
api.get<WeiboCollabAccountOption[], WeiboCollabAccountOption[]>(
'/weibo/external-orders/accounts/options',
),
summary: (params: { include_void?: boolean; archive_scope?: WeiboCollabArchiveScope }) =>
api.get<WeiboCollabSummary, WeiboCollabSummary>('/weibo/external-orders/summary', {
params,
}),
owners: () =>
api.get<WeiboCollabOwner[], WeiboCollabOwner[]>('/weibo/external-orders/owners'),
listOrders: (params: Record<string, unknown>) =>
api.get<WeiboCollabOrderList, WeiboCollabOrderList>('/weibo/external-orders', {
params,
}),
getOrder: (id: number) =>
api.get<WeiboCollabOrder, WeiboCollabOrder>(`/weibo/external-orders/${id}`),
createOrder: (data: WeiboCollabOrderCreate) =>
api.post<WeiboCollabOrder, WeiboCollabOrder>('/weibo/external-orders', data, {
timeout: 120000,
}),
updateOrder: (id: number, data: WeiboCollabOrderUpdate) =>
api.put<WeiboCollabOrder, WeiboCollabOrder>(`/weibo/external-orders/${id}`, data),
setAccount: (id: number, accountId: number, expectedVersion?: number) =>
api.post<WeiboCollabOrder, WeiboCollabOrder>(
`/weibo/external-orders/${id}/account`,
{ account_id: accountId, expected_version: expectedVersion },
{ timeout: 120000 },
),
verifyOrder: (id: number) =>
api.post<WeiboCollabOrder, WeiboCollabOrder>(
`/weibo/external-orders/${id}/verify`,
undefined,
{ timeout: 120000 },
),
updateStatus: (
id: number,
data: {
status: WeiboCollabStatus;
reason?: string;
rollback?: boolean;
expected_version?: number;
},
) =>
api.post<WeiboCollabOrder, WeiboCollabOrder>(
`/weibo/external-orders/${id}/status`,
data,
),
batchStatus: (data: {
ids: number[];
status: WeiboCollabStatus;
reason?: string;
rollback?: boolean;
}) => api.post('/weibo/external-orders/status/batch', data),
verifyBatch: (ids: number[]) =>
api.post<WeiboCollabVerifyBatchResult, WeiboCollabVerifyBatchResult>(
'/weibo/external-orders/verify/batch',
ids,
{ timeout: 120000 },
),
checkBalance: (ids: number[], threshold: number) =>
api.post<WeiboCollabBalanceCheckResult, WeiboCollabBalanceCheckResult>(
'/weibo/external-orders/balances/check',
{ ids, threshold },
{ timeout: 120000 },
),
checkExchangeRecords: (ids: number[]) =>
api.post<WeiboCollabExchangeCheckResult, WeiboCollabExchangeCheckResult>(
'/weibo/external-orders/exchange-records/check',
{ ids },
{ timeout: 120000 },
),
archive: (ids: number[], reason = '') =>
api.post<WeiboCollabArchiveResult, WeiboCollabArchiveResult>(
'/weibo/external-orders/archive',
{ ids, reason },
),
unarchive: (ids: number[]) =>
api.post<WeiboCollabUnarchiveResult, WeiboCollabUnarchiveResult>(
'/weibo/external-orders/unarchive',
ids,
),
deleteOrder: (id: number) => api.delete(`/weibo/external-orders/${id}`),
metrics: (id: number) =>
api.get<WeiboCollabMetric[], WeiboCollabMetric[]>(
`/weibo/external-orders/${id}/metrics`,
),
timeline: (id: number) =>
api.get<WeiboCollabTimeline[], WeiboCollabTimeline[]>(
`/weibo/external-orders/${id}/timeline`,
),
lookupAccount: (accountId: number, withGoods = true) =>
api.post<WeiboCollabAccountLookup, WeiboCollabAccountLookup>(
'/weibo/external-orders/accounts/lookup',
{ account_id: accountId, with_goods: withGoods },
{ timeout: 120000 },
),
listShops: () =>
api.get<WeiboCollabShop[], WeiboCollabShop[]>('/weibo/collab-shops'),
createShop: (data: { name: string; remark?: string }) =>
api.post<WeiboCollabShop, WeiboCollabShop>('/weibo/collab-shops', data),
updateShop: (id: number, data: Partial<Omit<WeiboCollabShop, 'id'>>) =>
api.put<WeiboCollabShop, WeiboCollabShop>(`/weibo/collab-shops/${id}`, data),
deleteShop: (id: number) => api.delete(`/weibo/collab-shops/${id}`),
listGoods: () =>
api.get<WeiboCollabGoods[], WeiboCollabGoods[]>('/weibo/collab-goods'),
createGoods: (data: {
name: string;
price: number;
goods_id?: number | null;
goods_image?: string;
remark?: string;
}) => api.post<WeiboCollabGoods, WeiboCollabGoods>('/weibo/collab-goods', data),
updateGoods: (id: number, data: Partial<Omit<WeiboCollabGoods, 'id'>>) =>
api.put<WeiboCollabGoods, WeiboCollabGoods>(`/weibo/collab-goods/${id}`, data),
deleteGoods: (id: number) => api.delete(`/weibo/collab-goods/${id}`),
};
+3
View File
@@ -129,6 +129,9 @@ export default function MainLayout({ onLogout }: { onLogout?: () => void }) {
if (can('weibo:view')) {
weiboItems.push({ key: '/weibo/accounts', label: '账号与绑定', icon: <MessageOutlined /> });
}
if (can('weibo:collab:view')) {
weiboItems.push({ key: '/weibo/collab', label: '外部单协作', icon: <FileExcelOutlined /> });
}
if (douyuItems.length > 0) {
menuItems.push({ key: 'group-douyu', type: 'group', label: '斗鱼', children: douyuItems });
File diff suppressed because it is too large Load Diff