feat(weibo): 增加工作台批量充值与批量导入
This commit is contained in:
@@ -118,6 +118,37 @@ UNIQUE(account_id) -- 同一账号同时只能属于一个工作台
|
||||
- 支持单个/批量移出工作台,不删除账号池数据。
|
||||
- 绑定二维码、充值、兑换、功能查询继续复用现有账号接口。
|
||||
|
||||
### 5.1 工作台批量充值(2026-11-11)
|
||||
|
||||
批量充值只放在「微博工作台」,不替换现有单账号充值:
|
||||
|
||||
1. 工作台勾选账号后点批量化栏的「批量充值」,填写本批统一金额(1~1000 元)。
|
||||
2. 每个账号调用一次现有 `POST /api/weibo/accounts/{id}/recharge/orders`,
|
||||
因此每个账号对应一张独立微博支付订单和一个支付宝二维码;
|
||||
微博没有原生批量下单接口,不能用一个二维码给多个账号充值。
|
||||
3. 前端受控并发为 3,单次最多 20 个账号;停用账号直接显示跳过原因,
|
||||
单个账号下单失败不影响其它账号。
|
||||
4. 支付码弹窗按账号分卡片展示二维码、金额、预计微豆、支付状态和到账状态;
|
||||
支持放大、复制支付链接、单项/批量刷新状态。
|
||||
5. 支付状态成功不等于微豆到账。刷新状态时会继续查询微豆明细,
|
||||
只有 `source_id == 充值 order_id` 且 `point_num > 0` 才显示「已到账」。
|
||||
6. 批量支付码弹窗打开时默认每 8 秒自动查询待支付/待确认到账项,最多 3 个并发;
|
||||
页面不可见或弹窗关闭时暂停,用户可手动关闭「自动查询」。
|
||||
微博没有独立扫码事件,自动查询从二维码可用后开始,支付成功后继续核对微豆到账。
|
||||
7. 二维码和支付链接只保存在当前页面内存,不写 localStorage 或数据库;
|
||||
刷新页面后需要重新生成。后续如果要求后台继续生成、刷新恢复或单次超过 20 个账号,
|
||||
再增加持久化批次任务和 WebSocket 进度。
|
||||
|
||||
### 5.2 从外部单协作复制账号标识
|
||||
|
||||
「微博外部单协作」列表和详情里的微博用户名、UID 都提供复制按钮。
|
||||
勾选多单后可一次性复制所选用户名或 UID,每行一个并自动去重。
|
||||
|
||||
工作台「导入账号」弹窗默认提供「批量粘贴」页签,支持换行、空格、逗号、
|
||||
中文逗号、分号分隔;后端先按 UID 精确匹配,再按用户名、角色名兜底解析,
|
||||
重名时必须改用 UID。导入结果逐项区分:已导入、已存在、未找到、重名、
|
||||
被其他工作台占用、账号停用和名单内重复。原来的「从账号池选择」页签完整保留。
|
||||
|
||||
## 6. 数据隔离边界
|
||||
|
||||
- 工作台列表和增删接口都按当前登录用户过滤。
|
||||
|
||||
@@ -32,6 +32,7 @@ from web.backend.routers.weibo import (
|
||||
enable_account,
|
||||
get_account,
|
||||
import_cookies,
|
||||
import_workbench_accounts_by_text,
|
||||
list_account_ids,
|
||||
list_account_tags,
|
||||
list_accounts,
|
||||
@@ -52,6 +53,7 @@ from web.backend.schemas import (
|
||||
WeiboPaymentStatusRequest,
|
||||
WeiboRechargeRequest,
|
||||
WeiboWorkbenchAccountsRequest,
|
||||
WeiboWorkbenchBatchImportRequest,
|
||||
)
|
||||
from web.backend.services import weibo_service
|
||||
|
||||
@@ -476,6 +478,92 @@ def test_weibo_workbench_is_isolated_per_user(db, current_user):
|
||||
assert [item.uid for item in pool_a["items"]] == [first.uid]
|
||||
|
||||
|
||||
def test_batch_import_workbench_accounts_reports_each_identifier(db, current_user):
|
||||
other_user = User(
|
||||
username="weibo-batch-import-other",
|
||||
password_hash="hash",
|
||||
role="support",
|
||||
is_active=True,
|
||||
)
|
||||
db.add(other_user)
|
||||
db.commit()
|
||||
|
||||
added = WeiboAccount(
|
||||
uid="4014000901",
|
||||
nickname="批量导入新账号",
|
||||
cookie="SCF=a; SUB=added",
|
||||
)
|
||||
existing = WeiboAccount(
|
||||
uid="4014000902",
|
||||
nickname="批量导入已存在",
|
||||
cookie="SCF=b; SUB=existing",
|
||||
)
|
||||
occupied = WeiboAccount(
|
||||
uid="4014000903",
|
||||
nickname="批量导入被占用",
|
||||
cookie="SCF=c; SUB=occupied",
|
||||
)
|
||||
disabled = WeiboAccount(
|
||||
uid="4014000904",
|
||||
nickname="批量导入停用",
|
||||
cookie="SCF=d; SUB=disabled",
|
||||
is_enabled=False,
|
||||
)
|
||||
duplicate_a = WeiboAccount(
|
||||
uid="4014000905",
|
||||
nickname="批量导入重名",
|
||||
cookie="SCF=e; SUB=duplicate-a",
|
||||
)
|
||||
duplicate_b = WeiboAccount(
|
||||
uid="4014000906",
|
||||
nickname="批量导入重名",
|
||||
cookie="SCF=f; SUB=duplicate-b",
|
||||
)
|
||||
db.add_all([added, existing, occupied, disabled, duplicate_a, duplicate_b])
|
||||
db.commit()
|
||||
db.add_all(
|
||||
[
|
||||
WeiboWorkbenchAccount(user_id=current_user.id, account_id=existing.id),
|
||||
WeiboWorkbenchAccount(user_id=other_user.id, account_id=occupied.id),
|
||||
]
|
||||
)
|
||||
db.commit()
|
||||
|
||||
result = import_workbench_accounts_by_text(
|
||||
WeiboWorkbenchBatchImportRequest(
|
||||
text=(
|
||||
f"{added.nickname} {added.uid} {existing.uid},{occupied.uid}\n"
|
||||
"不存在账号;批量导入重名 批量导入停用"
|
||||
)
|
||||
),
|
||||
db=db,
|
||||
current=current_user,
|
||||
).model_dump()
|
||||
|
||||
assert result["requested"] == 7
|
||||
assert result["added"] == 1
|
||||
assert result["already_added"] == 1
|
||||
assert result["failed"] == 5
|
||||
assert result["account_ids"] == [added.id]
|
||||
statuses = {item["identifier"]: item["status"] for item in result["items"]}
|
||||
assert statuses == {
|
||||
added.nickname: "added",
|
||||
added.uid: "duplicate",
|
||||
existing.uid: "already_added",
|
||||
occupied.uid: "unavailable",
|
||||
"不存在账号": "not_found",
|
||||
"批量导入重名": "ambiguous",
|
||||
"批量导入停用": "disabled",
|
||||
}
|
||||
workbench_ids = {
|
||||
account_id
|
||||
for (account_id,) in db.query(WeiboWorkbenchAccount.account_id)
|
||||
.filter(WeiboWorkbenchAccount.user_id == current_user.id)
|
||||
.all()
|
||||
}
|
||||
assert workbench_ids == {existing.id, added.id}
|
||||
|
||||
|
||||
def test_batch_tag_updates_selected_accounts(db, current_user):
|
||||
first = WeiboAccount(
|
||||
uid="4014000201",
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from loguru import logger
|
||||
@@ -41,6 +42,9 @@ from ..schemas import (
|
||||
WeiboRechargeOrderOut,
|
||||
WeiboRechargeRequest,
|
||||
WeiboWorkbenchAccountsRequest,
|
||||
WeiboWorkbenchBatchImportItem,
|
||||
WeiboWorkbenchBatchImportOut,
|
||||
WeiboWorkbenchBatchImportRequest,
|
||||
)
|
||||
from ..services.audit_service import record_audit
|
||||
from ..services.weibo_service import (
|
||||
@@ -61,6 +65,7 @@ from ..services.weibo_service import (
|
||||
|
||||
router = APIRouter(prefix="/api/weibo", tags=["微博微豆"])
|
||||
WEIBO_DEFAULT_ACTIVITY = os.getenv("WEIBO_DEFAULT_ACTIVITY", "").strip()
|
||||
WEIBO_WORKBENCH_BATCH_IMPORT_MAX = 200
|
||||
|
||||
|
||||
def _require_weibo_perm(user: User, permission: str) -> None:
|
||||
@@ -319,6 +324,175 @@ def list_workbench_pool(
|
||||
return _paginated_accounts(query, page=page, page_size=page_size)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/workbench/accounts/batch-import",
|
||||
response_model=WeiboWorkbenchBatchImportOut,
|
||||
)
|
||||
def import_workbench_accounts_by_text(
|
||||
req: WeiboWorkbenchBatchImportRequest,
|
||||
db: Session = Depends(get_db),
|
||||
current: User = Depends(get_current_user),
|
||||
):
|
||||
"""按 UID/用户名/角色批量解析账号并加入当前用户工作台。"""
|
||||
|
||||
_require_weibo_perm(current, "weibo:workbench")
|
||||
identifiers: list[str] = []
|
||||
for token in re.split(r"[\s,,;;]+", req.text.strip()):
|
||||
value = token.strip()
|
||||
if value and value not in identifiers:
|
||||
identifiers.append(value)
|
||||
if not identifiers:
|
||||
raise HTTPException(status_code=400, detail="请先粘贴微博用户名或 UID")
|
||||
if len(identifiers) > WEIBO_WORKBENCH_BATCH_IMPORT_MAX:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"一次最多导入 {WEIBO_WORKBENCH_BATCH_IMPORT_MAX} 个账号",
|
||||
)
|
||||
|
||||
visible_query = _visible_weibo_accounts_query(db.query(WeiboAccount), current)
|
||||
accounts = visible_query.filter(
|
||||
or_(
|
||||
WeiboAccount.uid.in_(identifiers),
|
||||
WeiboAccount.nickname.in_(identifiers),
|
||||
WeiboAccount.game_role_name.in_(identifiers),
|
||||
)
|
||||
).all()
|
||||
|
||||
def grouped(field: str) -> dict[str, list[WeiboAccount]]:
|
||||
result: dict[str, list[WeiboAccount]] = {}
|
||||
for account in accounts:
|
||||
value = str(getattr(account, field) or "").strip()
|
||||
if value:
|
||||
result.setdefault(value, []).append(account)
|
||||
return result
|
||||
|
||||
by_uid = grouped("uid")
|
||||
by_nickname = grouped("nickname")
|
||||
by_role = grouped("game_role_name")
|
||||
account_ids = [account.id for account in accounts]
|
||||
occupancy = {
|
||||
account_id: user_id
|
||||
for account_id, user_id in db.query(
|
||||
WeiboWorkbenchAccount.account_id,
|
||||
WeiboWorkbenchAccount.user_id,
|
||||
)
|
||||
.filter(WeiboWorkbenchAccount.account_id.in_(account_ids))
|
||||
.all()
|
||||
}
|
||||
|
||||
items: list[WeiboWorkbenchBatchImportItem] = []
|
||||
to_add: list[WeiboAccount] = []
|
||||
planned_ids: set[int] = set()
|
||||
for identifier in identifiers:
|
||||
candidates = by_uid.get(identifier) or by_nickname.get(identifier) or []
|
||||
if not candidates:
|
||||
candidates = by_role.get(identifier) or []
|
||||
if not candidates:
|
||||
items.append(
|
||||
WeiboWorkbenchBatchImportItem(
|
||||
identifier=identifier,
|
||||
status="not_found",
|
||||
message="当前可见账号池中没有匹配项",
|
||||
)
|
||||
)
|
||||
continue
|
||||
if len(candidates) > 1 and identifier not in by_uid:
|
||||
items.append(
|
||||
WeiboWorkbenchBatchImportItem(
|
||||
identifier=identifier,
|
||||
status="ambiguous",
|
||||
message=f"匹配到 {len(candidates)} 个账号,请改用 UID",
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
account = candidates[0]
|
||||
base = {
|
||||
"identifier": identifier,
|
||||
"account_id": account.id,
|
||||
"uid": account.uid,
|
||||
"nickname": account.nickname,
|
||||
}
|
||||
occupant = occupancy.get(account.id)
|
||||
if not account.is_enabled:
|
||||
items.append(
|
||||
WeiboWorkbenchBatchImportItem(
|
||||
status="disabled",
|
||||
message="账号已停用",
|
||||
**base,
|
||||
)
|
||||
)
|
||||
elif occupant is not None and occupant != current.id:
|
||||
items.append(
|
||||
WeiboWorkbenchBatchImportItem(
|
||||
status="unavailable",
|
||||
message="账号已在其他人的工作台中使用",
|
||||
**base,
|
||||
)
|
||||
)
|
||||
elif occupant == current.id:
|
||||
items.append(
|
||||
WeiboWorkbenchBatchImportItem(
|
||||
status="already_added",
|
||||
message="账号已在本工作台",
|
||||
**base,
|
||||
)
|
||||
)
|
||||
elif account.id in planned_ids:
|
||||
items.append(
|
||||
WeiboWorkbenchBatchImportItem(
|
||||
status="duplicate",
|
||||
message="同一账号在本次名单中重复",
|
||||
**base,
|
||||
)
|
||||
)
|
||||
else:
|
||||
planned_ids.add(account.id)
|
||||
to_add.append(account)
|
||||
items.append(
|
||||
WeiboWorkbenchBatchImportItem(
|
||||
status="added",
|
||||
message="已加入工作台",
|
||||
**base,
|
||||
)
|
||||
)
|
||||
|
||||
try:
|
||||
db.add_all(
|
||||
[
|
||||
WeiboWorkbenchAccount(user_id=current.id, account_id=account.id)
|
||||
for account in to_add
|
||||
]
|
||||
)
|
||||
db.commit()
|
||||
except IntegrityError as exc:
|
||||
db.rollback()
|
||||
raise HTTPException(
|
||||
status_code=409,
|
||||
detail="部分账号刚被其他工作台占用,请刷新后重试",
|
||||
) from exc
|
||||
if to_add:
|
||||
_record_audit_safely(
|
||||
db,
|
||||
current,
|
||||
action="weibo:workbench:batch_import",
|
||||
target=f"weibo_user:{current.id}",
|
||||
detail={"account_ids": [account.id for account in to_add]},
|
||||
)
|
||||
|
||||
added = sum(item.status == "added" for item in items)
|
||||
already_added = sum(item.status == "already_added" for item in items)
|
||||
failed = len(items) - added - already_added
|
||||
return WeiboWorkbenchBatchImportOut(
|
||||
requested=len(identifiers),
|
||||
added=added,
|
||||
already_added=already_added,
|
||||
failed=failed,
|
||||
account_ids=[account.id for account in to_add],
|
||||
items=items,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/workbench/tags")
|
||||
def list_workbench_tags(
|
||||
db: Session = Depends(get_db),
|
||||
|
||||
@@ -521,6 +521,28 @@ class WeiboWorkbenchAccountsRequest(BaseModel):
|
||||
account_ids: list[int] = Field(default_factory=list)
|
||||
|
||||
|
||||
class WeiboWorkbenchBatchImportRequest(BaseModel):
|
||||
text: str = Field(..., min_length=1, max_length=100_000)
|
||||
|
||||
|
||||
class WeiboWorkbenchBatchImportItem(BaseModel):
|
||||
identifier: str
|
||||
status: str
|
||||
message: str = ""
|
||||
account_id: int | None = None
|
||||
uid: str = ""
|
||||
nickname: str = ""
|
||||
|
||||
|
||||
class WeiboWorkbenchBatchImportOut(BaseModel):
|
||||
requested: int
|
||||
added: int
|
||||
already_added: int
|
||||
failed: int
|
||||
account_ids: list[int] = Field(default_factory=list)
|
||||
items: list[WeiboWorkbenchBatchImportItem] = Field(default_factory=list)
|
||||
|
||||
|
||||
class WeiboBatchAssignRequest(BaseModel):
|
||||
account_ids: list[int] = Field(min_length=1)
|
||||
assigned_to: int | None = None
|
||||
|
||||
@@ -814,6 +814,31 @@ export interface WeiboWorkbenchMutationResult {
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
export interface WeiboWorkbenchBatchImportItem {
|
||||
identifier: string;
|
||||
status:
|
||||
| 'added'
|
||||
| 'already_added'
|
||||
| 'not_found'
|
||||
| 'ambiguous'
|
||||
| 'unavailable'
|
||||
| 'disabled'
|
||||
| 'duplicate';
|
||||
message: string;
|
||||
account_id: number | null;
|
||||
uid: string;
|
||||
nickname: string;
|
||||
}
|
||||
|
||||
export interface WeiboWorkbenchBatchImportResult {
|
||||
requested: number;
|
||||
added: number;
|
||||
already_added: number;
|
||||
failed: number;
|
||||
account_ids: number[];
|
||||
items: WeiboWorkbenchBatchImportItem[];
|
||||
}
|
||||
|
||||
export interface WeiboBatchTagResult {
|
||||
updated: number;
|
||||
tag: string;
|
||||
|
||||
@@ -15,6 +15,7 @@ import type {
|
||||
WeiboRechargeOrderResult,
|
||||
WeiboImportResult,
|
||||
WeiboWorkbenchMutationResult,
|
||||
WeiboWorkbenchBatchImportResult,
|
||||
} from './types';
|
||||
|
||||
export const weiboApi = {
|
||||
@@ -122,6 +123,11 @@ export const weiboApi = {
|
||||
'/weibo/workbench/accounts',
|
||||
{ account_ids: accountIds },
|
||||
),
|
||||
batchImportWorkbenchAccounts: (text: string) =>
|
||||
api.post<WeiboWorkbenchBatchImportResult, WeiboWorkbenchBatchImportResult>(
|
||||
'/weibo/workbench/accounts/batch-import',
|
||||
{ text },
|
||||
),
|
||||
removeWorkbenchAccounts: (accountIds: number[]) =>
|
||||
api.post<WeiboWorkbenchMutationResult, WeiboWorkbenchMutationResult>(
|
||||
'/weibo/workbench/accounts/remove',
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -24,6 +24,7 @@ import {
|
||||
} from 'antd';
|
||||
import type { TableProps } from 'antd';
|
||||
import {
|
||||
CopyOutlined,
|
||||
DeleteOutlined,
|
||||
EditOutlined,
|
||||
FileExcelOutlined,
|
||||
@@ -58,6 +59,7 @@ import { useCollabRealtime } from '../hooks/useCollabRealtime';
|
||||
import { usePermissions } from '../hooks/usePermissions';
|
||||
import { getErrorMessage } from '../utils/error';
|
||||
import { message } from '../utils/antdMessage';
|
||||
import { copyTextToClipboard } from '../utils/clipboard';
|
||||
import { formatTime } from '../utils/time';
|
||||
|
||||
const { Text, Title } = Typography;
|
||||
@@ -578,6 +580,38 @@ export default function WeiboCollabPage() {
|
||||
[summary],
|
||||
);
|
||||
|
||||
const copySelectedAccountValues = async (kind: 'username' | 'uid') => {
|
||||
const selectedOrders = orders.filter((row) => selectedIds.includes(row.id));
|
||||
const values = Array.from(
|
||||
new Set(
|
||||
selectedOrders
|
||||
.map((row) =>
|
||||
kind === 'uid'
|
||||
? row.account_uid
|
||||
: row.account_nickname || row.account_uid,
|
||||
)
|
||||
.map((value) => String(value || '').trim())
|
||||
.filter(Boolean),
|
||||
),
|
||||
);
|
||||
if (values.length === 0) {
|
||||
message.warning(
|
||||
kind === 'uid' ? '所选订单没有微博 UID' : '所选订单没有微博用户名',
|
||||
);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await copyTextToClipboard(values.join('\n'));
|
||||
message.success(
|
||||
`已复制 ${values.length} 个${
|
||||
kind === 'uid' ? ' UID' : '用户名'
|
||||
},可直接粘贴到工作台导入搜索框`,
|
||||
);
|
||||
} catch (error) {
|
||||
message.error(getErrorMessage(error));
|
||||
}
|
||||
};
|
||||
|
||||
const columns: TableProps<WeiboCollabOrder>['columns'] = [
|
||||
{
|
||||
title: '序号',
|
||||
@@ -618,9 +652,33 @@ export default function WeiboCollabPage() {
|
||||
width: 210,
|
||||
render: (_, row) => (
|
||||
<Space orientation="vertical" size={0}>
|
||||
<Text>{row.account_nickname || row.account_uid || '未关联'}</Text>
|
||||
<Text
|
||||
copyable={
|
||||
row.account_nickname || row.account_uid
|
||||
? {
|
||||
text: row.account_nickname || row.account_uid || '',
|
||||
tooltips: ['复制用户名', '已复制用户名'],
|
||||
}
|
||||
: false
|
||||
}
|
||||
>
|
||||
{row.account_nickname || row.account_uid || '未关联'}
|
||||
</Text>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
UID {row.account_uid || '-'}
|
||||
{row.account_uid ? (
|
||||
<Text
|
||||
type="secondary"
|
||||
style={{ fontSize: 12 }}
|
||||
copyable={{
|
||||
text: row.account_uid,
|
||||
tooltips: ['复制 UID', '已复制 UID'],
|
||||
}}
|
||||
>
|
||||
UID {row.account_uid}
|
||||
</Text>
|
||||
) : (
|
||||
'UID -'
|
||||
)}
|
||||
</Text>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
{row.game_role_name || '未读到角色'}
|
||||
@@ -900,6 +958,22 @@ export default function WeiboCollabPage() {
|
||||
|
||||
<Space wrap>
|
||||
<Text type="secondary">已选 {selectedIds.length} 单</Text>
|
||||
{selectedIds.length > 0 && (
|
||||
<>
|
||||
<Button
|
||||
icon={<CopyOutlined />}
|
||||
onClick={() => void copySelectedAccountValues('username')}
|
||||
>
|
||||
复制所选用户名
|
||||
</Button>
|
||||
<Button
|
||||
icon={<CopyOutlined />}
|
||||
onClick={() => void copySelectedAccountValues('uid')}
|
||||
>
|
||||
复制所选 UID
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{canEdit && selectedIds.length > 0 && archiveScope !== 'archived' && (
|
||||
<>
|
||||
<Button
|
||||
@@ -1168,7 +1242,40 @@ export default function WeiboCollabPage() {
|
||||
column={2}
|
||||
items={[
|
||||
{ key: 'shop', label: '店铺 / 负责人', children: `${detail.shop || '-'} / ${detail.owner_name || '-'}` },
|
||||
{ key: 'account', label: '微博账号', children: `${detail.account_nickname || '-'} · UID ${detail.account_uid || '-'}` },
|
||||
{
|
||||
key: 'account',
|
||||
label: '微博账号',
|
||||
children: (
|
||||
<Space size={4} wrap>
|
||||
<Text
|
||||
copyable={
|
||||
detail.account_nickname || detail.account_uid
|
||||
? {
|
||||
text:
|
||||
detail.account_nickname ||
|
||||
detail.account_uid ||
|
||||
'',
|
||||
tooltips: ['复制用户名', '已复制用户名'],
|
||||
}
|
||||
: false
|
||||
}
|
||||
>
|
||||
{detail.account_nickname || '-'}
|
||||
</Text>
|
||||
{detail.account_uid && (
|
||||
<Text
|
||||
type="secondary"
|
||||
copyable={{
|
||||
text: detail.account_uid,
|
||||
tooltips: ['复制 UID', '已复制 UID'],
|
||||
}}
|
||||
>
|
||||
UID {detail.account_uid}
|
||||
</Text>
|
||||
)}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{ key: 'role', label: '绑定角色', children: detail.game_role_name || '-' },
|
||||
{ key: 'goods', label: '商品 / 所需微豆', children: `${detail.goods_name || '-'} / ${detail.required_weidou ?? '-'}` },
|
||||
{ key: 'status', label: '业务状态', children: <Tag color={STATUS_META[detail.status]?.color}>{detail.status_label}</Tag> },
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
export async function runWithConcurrency<T>(
|
||||
items: T[],
|
||||
limit: number,
|
||||
worker: (item: T) => Promise<void>,
|
||||
) {
|
||||
let nextIndex = 0;
|
||||
const workerCount = Math.min(Math.max(1, limit), items.length);
|
||||
await Promise.all(
|
||||
Array.from({ length: workerCount }, async () => {
|
||||
while (nextIndex < items.length) {
|
||||
const index = nextIndex;
|
||||
nextIndex += 1;
|
||||
await worker(items[index]);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user