feat(huya): 外部 CK 协作台账(S1–S6)+ 图片上传能力
协作台账:新表 huya_external_orders 替代 Excel 多人协同表(设计见
docs/设计-虎牙外部CK协作台账.md),一条 CK 一行,所有人共享、改动留痕。
- 前端 pages/HuyaCollabPage.tsx + 菜单「虎牙 → 外部单协作」/huya/collab:
内联编辑(带乐观锁 version)、图片缩略图、详情抽屉(字段表单 + 两个图片槽位 +
操作时间线)、粘贴导入弹窗(带 TSV 预览)、批量/单条验证 CK、页头统计条、筛选
- 两套状态机:业务 pending→bound→recharged→scripted→done(旁路 void)、
凭据 unverified/valid/invalid/disabled;唯一写入口在 services/huya_collab_service.py,
字段级审计 + 整数 version 乐观锁(不用 updated_at:DATETIME 只到秒)
- CK 只读探针 check_user_bind_game_account(真机判据:响应非 None 且 status == 200),
成功即覆盖写绑定快照 bind_game_account/bind_game_role/bind_game_channel(迁移 0056),
失败只改凭据状态、不动业务状态;人工只能停用/恢复(POST /{id}/ck/status)
- 贴/换/重建 CK、TSV 批量导入(SAVEPOINT 单行隔离,部分失败不整体失败)、
权限 huya:collab:view/edit/manage(删除只给运营)
- 孤儿图清理脚本 scripts/cleanup_orphan_file_assets.py(默认 --dry-run)
图片上传(协作台账截图用):OSS/本地双后端 + 缩略图变体 + /api/files 代理回读,
权限 file:upload / file:view。
验证:pytest 880 passed / 6 skipped;ruff format 全绿、ruff check 仅剩既有告警;
前端 tsc -b / eslint / vite build 通过;接口端到端冒烟 29 项全绿(TestClient,探针用假客户端);
探针真机冒烟 3 例(有效 CK=True、别人 uid=False、裸 cookie=False)。
This commit is contained in:
@@ -28,6 +28,7 @@ const HuyaRegisterPage = lazy(() => import('./pages/HuyaRegisterPage'));
|
||||
const HuyaDeviceBindingsPage = lazy(() => import('./pages/HuyaDeviceBindingsPage'));
|
||||
const HuyaTasksPage = lazy(() => import('./pages/HuyaTasksPage'));
|
||||
const HuyaElitePage = lazy(() => import('./pages/HuyaElitePage'));
|
||||
const HuyaCollabPage = lazy(() => import('./pages/HuyaCollabPage'));
|
||||
const HuyaBrowserExchangePage = lazy(() => import('./pages/HuyaBrowserExchangePage'));
|
||||
const YybRechargePage = lazy(() => import('./pages/YybRechargePage'));
|
||||
const AuditLogsPage = lazy(() => import('./pages/AuditLogsPage'));
|
||||
@@ -100,6 +101,7 @@ function AppContent() {
|
||||
<Route path="huya/cookies" element={lazyRoute(<HuyaCookiePage />)} />
|
||||
<Route path="huya/tasks" element={lazyRoute(<HuyaTasksPage />)} />
|
||||
<Route path="huya/elite" element={lazyRoute(<HuyaElitePage />)} />
|
||||
<Route path="huya/collab" element={lazyRoute(<HuyaCollabPage />)} />
|
||||
<Route path="huya/browser-exchange" element={lazyRoute(<HuyaBrowserExchangePage />)} />
|
||||
<Route path="yyb/recharge" element={lazyRoute(<YybRechargePage />)} />
|
||||
<Route path="proxy" element={lazyRoute(<ProxyPage />)} />
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import api from './client';
|
||||
import type { FileUploadResult } from './types';
|
||||
|
||||
/**
|
||||
* 文件上传/回读(后端 /api/files)。
|
||||
*
|
||||
* 图片一律经后端代理地址回读(/api/files/object?key=...),不下发对象存储直链,
|
||||
* 也不在前端存签名 URL(签名会过期,过期后到处裂图)。
|
||||
*/
|
||||
export const fileApi = {
|
||||
/** 上传一张图片,返回 object_key(协作台账图片槽位提交的就是它) */
|
||||
upload: (file: File, scene = 'collab') => {
|
||||
const form = new FormData();
|
||||
form.append('file', file);
|
||||
form.append('scene', scene);
|
||||
return api.post<FileUploadResult, FileUploadResult>('/files/upload', form);
|
||||
},
|
||||
};
|
||||
@@ -31,6 +31,20 @@ import type {
|
||||
HuyaTaskBatchResult,
|
||||
HuyaTaskItem,
|
||||
HuyaBrowserExchangeKit,
|
||||
HuyaCollabAuditItem,
|
||||
HuyaCollabCkRequest,
|
||||
HuyaCollabCkStatusRequest,
|
||||
HuyaCollabImagesRequest,
|
||||
HuyaCollabImportResult,
|
||||
HuyaCollabOrder,
|
||||
HuyaCollabOrderCreate,
|
||||
HuyaCollabOrderList,
|
||||
HuyaCollabOrderQuery,
|
||||
HuyaCollabOrderUpdate,
|
||||
HuyaCollabOwner,
|
||||
HuyaCollabStatusRequest,
|
||||
HuyaCollabSummary,
|
||||
HuyaCollabVerifyResult,
|
||||
HuyaExchangeTargetKey,
|
||||
HuyaExternalCkImportRequest,
|
||||
HuyaExternalCkImportResult,
|
||||
@@ -172,4 +186,53 @@ export const huyaApi = {
|
||||
/** 本机浏览器兑换: 获取目标页与登录脚本, 由用户在自己浏览器的控制台执行 (target 不传 = elite) */
|
||||
createBrowserExchange: (accountId: number, target?: HuyaExchangeTargetKey) =>
|
||||
api.post<HuyaBrowserExchangeKit, HuyaBrowserExchangeKit>(`/huya/accounts/${accountId}/browser-exchange`, { target }),
|
||||
|
||||
// ---- 虎牙外部 CK 协作台账(共享表,替代 Excel;业务规则见 docs/设计-虎牙外部CK协作台账.md)----
|
||||
/** 列表:共享视图,默认不按人过滤(负责人只是筛选维度) */
|
||||
listCollabOrders: (params: HuyaCollabOrderQuery = {}) =>
|
||||
api.get<HuyaCollabOrderList, HuyaCollabOrderList>('/huya/external-orders', { params }),
|
||||
/** 页头统计条:各业务状态计数 + CK 失效数 */
|
||||
collabOrdersSummary: () =>
|
||||
api.get<HuyaCollabSummary, HuyaCollabSummary>('/huya/external-orders/summary'),
|
||||
/** 负责人候选(全部活跃用户) */
|
||||
collabOrderOwners: () =>
|
||||
api.get<HuyaCollabOwner[], HuyaCollabOwner[]>('/huya/external-orders/owners'),
|
||||
/** 已用过的店铺(筛选下拉) */
|
||||
collabOrderShops: () => api.get<string[], string[]>('/huya/external-orders/shops'),
|
||||
getCollabOrder: (id: number) =>
|
||||
api.get<HuyaCollabOrder, HuyaCollabOrder>(`/huya/external-orders/${id}`),
|
||||
createCollabOrder: (data: HuyaCollabOrderCreate) =>
|
||||
api.post<HuyaCollabOrder, HuyaCollabOrder>('/huya/external-orders', data),
|
||||
/** 编辑:必须带 expected_version(别人先改过会 409) */
|
||||
updateCollabOrder: (id: number, data: HuyaCollabOrderUpdate) =>
|
||||
api.put<HuyaCollabOrder, HuyaCollabOrder>(`/huya/external-orders/${id}`, data),
|
||||
/** 推进/回退业务状态(回退必须显式 rollback) */
|
||||
updateCollabOrderStatus: (id: number, data: HuyaCollabStatusRequest) =>
|
||||
api.post<HuyaCollabOrder, HuyaCollabOrder>(`/huya/external-orders/${id}/status`, data),
|
||||
/** 贴/换 CK(action=paste)或从 raw_cookie 重建(action=rebuild) */
|
||||
updateCollabOrderCk: (id: number, data: HuyaCollabCkRequest) =>
|
||||
api.post<HuyaCollabOrder, HuyaCollabOrder>(`/huya/external-orders/${id}/ck`, data),
|
||||
/** 人工停用 / 恢复这份 CK(valid / invalid 只能由探针给) */
|
||||
updateCollabOrderCkStatus: (id: number, data: HuyaCollabCkStatusRequest) =>
|
||||
api.post<HuyaCollabOrder, HuyaCollabOrder>(`/huya/external-orders/${id}/ck/status`, data),
|
||||
/** 整槽替换图片(提交 /api/files/upload 返回的 object_key 数组) */
|
||||
updateCollabOrderImages: (id: number, data: HuyaCollabImagesRequest) =>
|
||||
api.post<HuyaCollabOrder, HuyaCollabOrder>(`/huya/external-orders/${id}/images`, data),
|
||||
/** 验证 CK(只读探针):写 ck_status,并刷新绑定账号/角色快照 */
|
||||
verifyCollabOrder: (id: number) =>
|
||||
api.post<HuyaCollabOrder, HuyaCollabOrder>(`/huya/external-orders/${id}/verify`),
|
||||
/** 批量验证 CK:人工触发、串行带间隔,一次最多 50 条 */
|
||||
verifyCollabOrders: (ids: number[]) =>
|
||||
api.post<HuyaCollabVerifyResult, HuyaCollabVerifyResult>('/huya/external-orders/ck/verify', { ids }),
|
||||
/** Excel/文本粘贴批量导入(TSV,首行可为表头) */
|
||||
importCollabOrders: (text: string) =>
|
||||
api.post<HuyaCollabImportResult, HuyaCollabImportResult>('/huya/external-orders/import', { text }),
|
||||
/** 操作时间线(直接读 audit_logs) */
|
||||
collabOrderTimeline: (id: number) =>
|
||||
api.get<HuyaCollabAuditItem[], HuyaCollabAuditItem[]>(`/huya/external-orders/${id}/timeline`),
|
||||
/** 删除一行(只给运营:huya:collab:manage) */
|
||||
deleteCollabOrder: (id: number) =>
|
||||
api.delete<{ success: boolean; order_no: string }, { success: boolean; order_no: string }>(
|
||||
`/huya/external-orders/${id}`,
|
||||
),
|
||||
};
|
||||
|
||||
@@ -8,6 +8,7 @@ export { automationApi } from './automation';
|
||||
export { cookieApi } from './cookies';
|
||||
export { dashboardApi } from './dashboard';
|
||||
export { douyuApi } from './douyu';
|
||||
export { fileApi } from './files';
|
||||
export { huyaApi } from './huya';
|
||||
export { loginApi } from './login';
|
||||
export { proxyApi } from './proxy';
|
||||
|
||||
@@ -1195,3 +1195,194 @@ export interface AccountExchangeRecords {
|
||||
total: number;
|
||||
queried_at: string | null;
|
||||
}
|
||||
|
||||
// ---- 虎牙外部 CK 协作台账(/api/huya/external-orders,见 docs/设计-虎牙外部CK协作台账.md)----
|
||||
|
||||
/** 业务状态:一单卡在哪一步(void 是旁路) */
|
||||
export type HuyaCollabStatus = 'pending' | 'bound' | 'recharged' | 'scripted' | 'done' | 'void';
|
||||
|
||||
/** 凭据状态:CK 本身还有没有效 */
|
||||
export type HuyaCollabCkStatus = 'unverified' | 'valid' | 'invalid' | 'disabled';
|
||||
|
||||
export type HuyaCollabImageSlot = 'bind_images' | 'home_images';
|
||||
|
||||
/** 一行 = 一单 = 一个外部虎牙 CK */
|
||||
export interface HuyaCollabOrder {
|
||||
id: number;
|
||||
order_no: string;
|
||||
shop: string;
|
||||
owner_id: number | null;
|
||||
owner_name: string;
|
||||
uid: string;
|
||||
yyuid: string;
|
||||
hy_username: string;
|
||||
/** 虎牙号昵称(探针给不出,暂时为空) */
|
||||
nickname: string;
|
||||
has_cookie: boolean;
|
||||
has_raw_cookie: boolean;
|
||||
ck_status: HuyaCollabCkStatus;
|
||||
ck_status_label: string;
|
||||
ck_checked_at: string | null;
|
||||
ck_check_message: string;
|
||||
/** 人工填的绑定斗鱼号(Excel D 列,贴表导入用) */
|
||||
bind_douyu_account: string;
|
||||
bind_douyu_uid: string;
|
||||
/** 探针读到的绑定对象(只读快照):游戏账号 / 角色 / 平台·区服 */
|
||||
bind_game_account: string;
|
||||
bind_game_role: string;
|
||||
bind_game_channel: string;
|
||||
bind_snapshot_at: string | null;
|
||||
bind_images: string[];
|
||||
home_images: string[];
|
||||
bind_image_urls: string[];
|
||||
home_image_urls: string[];
|
||||
bind_image_thumb_urls: string[];
|
||||
home_image_thumb_urls: string[];
|
||||
gift_name: string;
|
||||
remark: string;
|
||||
status: HuyaCollabStatus;
|
||||
status_label: string;
|
||||
next_status: HuyaCollabStatus | null;
|
||||
bound_at: string | null;
|
||||
recharged_at: string | null;
|
||||
scripted_at: string | null;
|
||||
done_at: string | null;
|
||||
void_reason: string;
|
||||
points: number | null;
|
||||
elite_opened: boolean | null;
|
||||
snapshot_at: string | null;
|
||||
created_by: number | null;
|
||||
created_by_name: string;
|
||||
updated_by: number | null;
|
||||
updated_by_name: string;
|
||||
/** 乐观锁版本号:提交时必须原样带回,别人先改过会 409 */
|
||||
version: number;
|
||||
created_at: string | null;
|
||||
updated_at: string | null;
|
||||
}
|
||||
|
||||
export interface HuyaCollabOrderList {
|
||||
items: HuyaCollabOrder[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
}
|
||||
|
||||
export interface HuyaCollabSummary {
|
||||
total: number;
|
||||
by_status: Record<string, number>;
|
||||
ck_invalid: number;
|
||||
}
|
||||
|
||||
export interface HuyaCollabOwner {
|
||||
id: number;
|
||||
username: string;
|
||||
}
|
||||
|
||||
export interface HuyaCollabAuditItem {
|
||||
id: number;
|
||||
action: string;
|
||||
username: string;
|
||||
detail: string;
|
||||
success: boolean | null;
|
||||
created_at: string | null;
|
||||
}
|
||||
|
||||
export interface HuyaCollabOrderQuery {
|
||||
shop?: string;
|
||||
owner_id?: number | null;
|
||||
status?: string;
|
||||
ck_status?: string;
|
||||
keyword?: string;
|
||||
include_void?: boolean;
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
}
|
||||
|
||||
export interface HuyaCollabOrderCreate {
|
||||
order_no: string;
|
||||
shop?: string;
|
||||
owner_id?: number | null;
|
||||
ck_text?: string;
|
||||
bind_douyu_account?: string;
|
||||
gift_name?: string;
|
||||
remark?: string;
|
||||
}
|
||||
|
||||
export interface HuyaCollabOrderUpdate {
|
||||
order_no?: string;
|
||||
shop?: string;
|
||||
owner_id?: number | null;
|
||||
bind_douyu_account?: string;
|
||||
bind_douyu_uid?: string;
|
||||
gift_name?: string;
|
||||
remark?: string;
|
||||
expected_version?: number;
|
||||
}
|
||||
|
||||
export interface HuyaCollabStatusRequest {
|
||||
status: HuyaCollabStatus;
|
||||
reason?: string;
|
||||
rollback?: boolean;
|
||||
expected_version?: number;
|
||||
}
|
||||
|
||||
export interface HuyaCollabCkRequest {
|
||||
ck_text: string;
|
||||
action: 'paste' | 'rebuild';
|
||||
expected_version?: number;
|
||||
}
|
||||
|
||||
/** 人工只能把 CK 标成「已停用」/「未验证」;valid/invalid 由探针给 */
|
||||
export interface HuyaCollabCkStatusRequest {
|
||||
status: 'unverified' | 'disabled';
|
||||
message?: string;
|
||||
expected_version?: number;
|
||||
}
|
||||
|
||||
export interface HuyaCollabImagesRequest {
|
||||
slot: HuyaCollabImageSlot;
|
||||
images: string[];
|
||||
expected_version?: number;
|
||||
}
|
||||
|
||||
export interface HuyaCollabImportError {
|
||||
line: number;
|
||||
order_no: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface HuyaCollabImportResult {
|
||||
created: number;
|
||||
updated: number;
|
||||
skipped: number;
|
||||
errors: HuyaCollabImportError[];
|
||||
parsed_columns: string[];
|
||||
}
|
||||
|
||||
export interface HuyaCollabVerifyItem {
|
||||
id: number;
|
||||
order_no: string;
|
||||
ok: boolean;
|
||||
ck_status: string;
|
||||
message: string;
|
||||
binding: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface HuyaCollabVerifyResult {
|
||||
total: number;
|
||||
valid: number;
|
||||
failed: number;
|
||||
results: HuyaCollabVerifyItem[];
|
||||
}
|
||||
|
||||
/** 上传接口返回(object_key 是后续提交给协作台账图片槽位的东西) */
|
||||
export interface FileUploadResult {
|
||||
object_key: string;
|
||||
url: string;
|
||||
thumbnail_url: string;
|
||||
medium_url: string;
|
||||
filename: string;
|
||||
content_type: string;
|
||||
size: number;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
MenuFoldOutlined, MenuUnfoldOutlined, SwapOutlined,
|
||||
SunOutlined, MoonOutlined, DesktopOutlined, GiftOutlined, ApartmentOutlined, MobileOutlined, ShoppingCartOutlined,
|
||||
BookOutlined, TrophyOutlined, ShopOutlined, GlobalOutlined,
|
||||
SafetyCertificateOutlined, FileSearchOutlined, RobotOutlined,
|
||||
SafetyCertificateOutlined, FileSearchOutlined, RobotOutlined, FileExcelOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { useNavigate, useLocation, Outlet } from 'react-router-dom';
|
||||
import { getUser, clearAuth, type AuthUser } from '../store/auth';
|
||||
@@ -113,6 +113,12 @@ export default function MainLayout({ onLogout }: { onLogout?: () => void }) {
|
||||
douyuItems.push({ key: '/yyb/recharge', label: '应用宝充值', icon: <ShoppingCartOutlined /> });
|
||||
}
|
||||
|
||||
// 外部单协作台账:权限独立(huya:collab:view/edit 给运营和客服),
|
||||
// 不受「虎牙任务」那套分支影响 —— 客服没有 huya:task 也要能看到这张共享表。
|
||||
if (can('huya:collab:view')) {
|
||||
huyaItems.push({ key: '/huya/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