feat(creator): 新增「运营」模块 —— 多账号扫码登录与创作者后台数据
侧边栏在「监控」右边加了「运营」:账号列表 → 点进二级详情看该账号的数据。 【为什么是独立模块而不是监控的子视图】两者形状不同:监控是公开数据(点赞/收藏/评论/分享)的每轮快照+差分;运营是创作者后台按日期给出的曝光/观看/完播率/涨粉。凭据不同、采集方式也不同 —— 那边要浏览器登录态,这边是纯请求。硬塞进同一个模型会同时污染两边。 【扫码登录的关键差异】监控的扫码把登录态写进浏览器默认 profile(爬虫要复用)。运营要的是 cookie 字符串(纯请求够用),所以每次登录开一个**临时上下文**,扫完取出 cookie 就丢弃 —— 登第二个账号不会把第一个顶掉,也不影响监控那个登录态,十个账号互不干扰。 【决策依据】tools/probe_creator_api.py 的 Phase 0 实测:签名可自造(XYW_:MD5 → base64 → AES-128-CBC,与 xhshow 内置实现常量逐字节一致);主站 cookie 即可认证创作者后台;接口与参数已与真实页面对齐。 后端: - api/creator/models.py: creator_account / creator_note_stat。**复用 MonitorBase**,这样 create_all 与上一轮改成元数据驱动的 _ensure_columns 会自动覆盖新表 - api/creator/signing.py: XYW_ 签名,带三条实测结论(url= 前缀、appId=ugc、401 与 406 的区别) - api/creator/client.py: 纯 httpx 客户端。字段名尚未亲眼验证过,所以写成多别名匹配;解析不出来存 None 而非 0 - api/creator/service.py: 账号 CRUD 与同步。cookie 绝不进入对外结构,只给 has_cookie - api/creator/login.py: 临时上下文的扫码登录 - api/routers/creator.py: 8 条路由,全部带鉴权 前端: - 侧边栏「运营」+ OperationView(账号列表 → 二级详情)+ AddAccountDialog - 权限状态显眼呈现:pending 时照抄后台原话「已为您申请数据权限,次日可查看」,并说明此时同步返回 0 条是正常的,不是采集失败 测试:tests/test_creator_client.py 新增 48 例,含「cookie 不得出现在对外结构里」这条不变量,以及权限未生效时空壳响应的处理。
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import { creatorApi } from '@/lib/api'
|
||||
|
||||
const ACCOUNTS_KEY = ['creatorAccounts']
|
||||
|
||||
/** 账号列表。同步在后台跑,所以列表本身也顺带轮询,好让状态自己刷新出来。 */
|
||||
export function useCreatorAccounts(polling = false) {
|
||||
return useQuery({
|
||||
queryKey: ACCOUNTS_KEY,
|
||||
queryFn: async () => (await creatorApi.listAccounts()).data.accounts,
|
||||
refetchInterval: polling ? 5000 : false,
|
||||
})
|
||||
}
|
||||
|
||||
export function useCreatorAccount(id: number | null) {
|
||||
return useQuery({
|
||||
queryKey: ['creatorAccount', id],
|
||||
queryFn: async () => (await creatorApi.getAccount(id as number)).data,
|
||||
enabled: id !== null,
|
||||
})
|
||||
}
|
||||
|
||||
export function useDeleteCreatorAccount() {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (id: number) => creatorApi.deleteAccount(id),
|
||||
onSuccess: () => {
|
||||
toast.success('账号已删除')
|
||||
queryClient.invalidateQueries({ queryKey: ACCOUNTS_KEY })
|
||||
},
|
||||
onError: (error: Error) => toast.error(`删除失败:${error.message}`),
|
||||
})
|
||||
}
|
||||
|
||||
export function useCheckCreatorAccount() {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (id: number) => creatorApi.checkAccount(id),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ACCOUNTS_KEY })
|
||||
},
|
||||
onError: (error: Error) => toast.error(`检测失败:${error.message}`),
|
||||
})
|
||||
}
|
||||
|
||||
export function useSyncCreatorAccount() {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: ({ id, days }: { id: number; days?: number }) =>
|
||||
creatorApi.syncAccount(id, days),
|
||||
onSuccess: () => {
|
||||
// 后端是后台任务,立刻重新拉一次只会看到旧状态;给用户一句"已开始",
|
||||
// 列表的轮询会把结果带回来。
|
||||
toast.success('同步已开始,稍后自动刷新')
|
||||
queryClient.invalidateQueries({ queryKey: ACCOUNTS_KEY })
|
||||
},
|
||||
onError: (error: Error) => toast.error(`同步失败:${error.message}`),
|
||||
})
|
||||
}
|
||||
|
||||
// --- 扫码新增账号 ---------------------------------------------------------
|
||||
|
||||
/** 只在扫码中轮询;空闲时没必要一直问。 */
|
||||
export function useCreatorLoginStatus(polling: boolean) {
|
||||
return useQuery({
|
||||
queryKey: ['creatorLogin'],
|
||||
queryFn: async () => (await creatorApi.getLogin()).data,
|
||||
refetchInterval: polling ? 2000 : false,
|
||||
})
|
||||
}
|
||||
|
||||
export function useStartCreatorLogin() {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: () => creatorApi.startLogin(),
|
||||
onSuccess: (response) => {
|
||||
queryClient.setQueryData(['creatorLogin'], response.data)
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
// 后端给的原因是这里唯一有价值的信息(连不上 Chrome、页面上没有二维码),
|
||||
// 而 axios 会把它压成 "Request failed with status code 502"。
|
||||
const detail = (error as { response?: { data?: { detail?: string } } })?.response?.data
|
||||
?.detail
|
||||
toast.error(`获取二维码失败:${detail ?? error.message}`)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useCancelCreatorLogin() {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: () => creatorApi.cancelLogin(),
|
||||
onSuccess: (response) => {
|
||||
queryClient.setQueryData(['creatorLogin'], response.data)
|
||||
},
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user