feat(weibo): 增加外部单协作台账
This commit is contained in:
@@ -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")
|
||||
@@ -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):
|
||||
"""虎牙自动注册批次(持久化元数据)"""
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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}
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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 />)} />
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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}`),
|
||||
};
|
||||
@@ -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
Reference in New Issue
Block a user