feat(channels): 按账号连接/断开 + 可删空 id 条目(修主人真机踩的两个缺口)
缺口①:「连接」是按平台连、且恒打第一条账号(channelActionRequest 只有 name)⇒ 主人有两条
qq-obv11 时点到的是 mode=server 的历史条目,报 server mode requires listenAddr,而正确的
client 条目轮不到。缺口②:空 id 条目既删不掉(DELETE 要求 id 非空)也改不了(UpsertChannel
空 id = 新建),越改越多。
后端:
- POST /admin/channels/open|close 请求体新增可选 accountId(缺省逐字兼容,老客户端零影响);
按该账号驱动;未知 id ⇒ 404 且零连接(不静默回落第一条);已配置未注册 ⇒ 409
- DELETE /admin/channels/{platform}/config 新增显式 empty=1(id 与 empty=1 互斥 ⇒ 400;
无可删 ⇒ 404);语义严格 —— 只删空 id 条目,绝不把「id 为空」默认解释成删空 id
- 响应新增:open/close 的 resolved(实际驱动的适配器名)与 accountId;delete 的 removed/empty;
GET /admin/channels 的 accounts[].connected(可选 bool,缺省 = 未知)
- 审计动作名不变(channel.open/close/delete),日志行新增 accountId 字段
前端:每条账号一行「连接/断开」(带 accountId);按钮旁明确「会连哪一条」;空 id 行显著标注
+ 删除入口;channelActionBody 携带 accountId;新增 action 测试 33 checks + 空 id 行渲染 31 checks。
测试:后端 14 个用例(真实 HTTP 路由 + JWT + 真注册表 + spy 适配器,断言「哪一条被驱动、
另一条一次都没被碰」)。
变异 6 条(accountId 被忽略 / id 为空当删空 id / 找不到静默回落 / 前端丢 accountId /
删空 id 退化成 ?id= / 空 id 行按普通行渲染)。
This commit is contained in:
1 parent
c4ea02a887
commit
42279b7124
21 files changed
+1719
-164
No files matched your search
@@ -1,11 +1,97 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import {
|
||||
adminChannelOpen,
|
||||
adminChannelClose,
|
||||
adminChannelOpen,
|
||||
adminDeleteChannelAccount,
|
||||
adminDeleteEmptyChannelAccounts,
|
||||
} from "../../../lib/api";
|
||||
import { useAuditStore } from "../../audit";
|
||||
|
||||
/** 渠道动作执行器需要的最小依赖:状态写入口 + 审计 + 刷新。
|
||||
* 抽成显式依赖是为了让「请求里到底带没带 accountId」能在 node 侧被断言
|
||||
* (见 channelActions.test.ts)—— 把这段逻辑埋在 React hook 里就测不到,
|
||||
* 而「accountId 被丢掉」正是这次要修的产品缺口本身。 */
|
||||
export interface ChannelActionDeps {
|
||||
log: (action: string, result: "ok" | "error") => void;
|
||||
setBusy: (busy: string | null) => void;
|
||||
setError: (error: string | null) => void;
|
||||
setNotice: (notice: string | null) => void;
|
||||
reload: () => void;
|
||||
}
|
||||
|
||||
/** 与 React 解耦的渠道动作集合(连接/断开/删除账号/删除无 id 条目)。 */
|
||||
export interface ChannelActions {
|
||||
/** 连接/断开。`accountId` 给出时**只**作用于该账号(多账号平台逐条操作);
|
||||
* 缺省时维持平台级旧行为(后端按自己的规则挑一条)。 */
|
||||
toggle: (name: string, connect: boolean, accountId?: string) => void;
|
||||
del: (platform: string, id: string) => void;
|
||||
/** 删除该平台**无 id 的历史条目**(显式 `?empty=1`)。 */
|
||||
delEmpty: (platform: string) => void;
|
||||
}
|
||||
|
||||
export function createChannelActions(deps: ChannelActionDeps): ChannelActions {
|
||||
const { log, setBusy, setError, setNotice, reload } = deps;
|
||||
|
||||
const toggle = (name: string, connect: boolean, accountId?: string) => {
|
||||
setBusy(name);
|
||||
setError(null);
|
||||
const call = connect ? adminChannelOpen(name, accountId) : adminChannelClose(name, accountId);
|
||||
call
|
||||
.then((res) => {
|
||||
log(connect ? "channel.open" : "channel.close", "ok");
|
||||
// 回显**实际被驱动**的适配器:多账号平台上请求里的平台名说明不了「连的是哪一条」,
|
||||
// 后端把解析结果放在 resolved 里,这里如实说出来。
|
||||
const who = res?.resolved ?? name;
|
||||
setNotice(`${connect ? "已连接" : "已断开"} ${who}`);
|
||||
reload();
|
||||
})
|
||||
.catch((e: unknown) => {
|
||||
log(connect ? "channel.open" : "channel.close", "error");
|
||||
setError(e instanceof Error ? e.message : `${connect ? "连接" : "断开"}失败`);
|
||||
})
|
||||
.finally(() => setBusy(null));
|
||||
};
|
||||
|
||||
const del = (platform: string, id: string) => {
|
||||
setBusy("__del__" + id);
|
||||
setError(null);
|
||||
setNotice(null);
|
||||
adminDeleteChannelAccount(platform, id)
|
||||
.then(() => {
|
||||
log("channel.delete", "ok");
|
||||
setNotice(`已删除账号 ${id} 并热更新 ${platform}`);
|
||||
reload();
|
||||
})
|
||||
.catch((e: unknown) => {
|
||||
log("channel.delete", "error");
|
||||
setError(e instanceof Error ? e.message : "删除渠道账号失败");
|
||||
})
|
||||
.finally(() => setBusy(null));
|
||||
};
|
||||
|
||||
const delEmpty = (platform: string) => {
|
||||
setBusy("__delEmpty__" + platform);
|
||||
setError(null);
|
||||
setNotice(null);
|
||||
adminDeleteEmptyChannelAccounts(platform)
|
||||
.then((res) => {
|
||||
log("channel.delete", "ok");
|
||||
// 删了几条必须说出来:无 id 条目彼此不可区分,「删掉无 id 条目」是批量动作,
|
||||
// 条数对调用方是唯一能核对的东西。
|
||||
const n = res?.removed ?? 0;
|
||||
setNotice(`已删除 ${n} 条无 id 条目并热更新 ${platform}`);
|
||||
reload();
|
||||
})
|
||||
.catch((e: unknown) => {
|
||||
log("channel.delete", "error");
|
||||
setError(e instanceof Error ? e.message : "删除无 id 条目失败");
|
||||
})
|
||||
.finally(() => setBusy(null));
|
||||
};
|
||||
|
||||
return { toggle, del, delEmpty };
|
||||
}
|
||||
|
||||
/** Shared connect/disconnect/delete actions + busy/error/notice state, reused by
|
||||
* the list page and each adapter detail page. `reload` re-fetches the channel
|
||||
* list after a mutation. */
|
||||
@@ -15,44 +101,10 @@ export function useChannelActions(reload: () => void) {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [notice, setNotice] = useState<string | null>(null);
|
||||
|
||||
const toggle = useCallback(
|
||||
(name: string, connect: boolean) => {
|
||||
setBusy(name);
|
||||
setError(null);
|
||||
const call = connect ? adminChannelOpen(name) : adminChannelClose(name);
|
||||
call
|
||||
.then(() => {
|
||||
log(connect ? "channel.open" : "channel.close", "ok");
|
||||
reload();
|
||||
})
|
||||
.catch((e: unknown) => {
|
||||
log(connect ? "channel.open" : "channel.close", "error");
|
||||
setError(e instanceof Error ? e.message : `${connect ? "连接" : "断开"}失败`);
|
||||
})
|
||||
.finally(() => setBusy(null));
|
||||
},
|
||||
[reload, log],
|
||||
const actions = useMemo(
|
||||
() => createChannelActions({ log, setBusy, setError, setNotice, reload }),
|
||||
[log, reload],
|
||||
);
|
||||
|
||||
const del = useCallback(
|
||||
(platform: string, id: string) => {
|
||||
setBusy("__del__" + id);
|
||||
setError(null);
|
||||
setNotice(null);
|
||||
adminDeleteChannelAccount(platform, id)
|
||||
.then(() => {
|
||||
log("channel.delete", "ok");
|
||||
setNotice(`已删除账号 ${id} 并热更新 ${platform}`);
|
||||
reload();
|
||||
})
|
||||
.catch((e: unknown) => {
|
||||
log("channel.delete", "error");
|
||||
setError(e instanceof Error ? e.message : "删除渠道账号失败");
|
||||
})
|
||||
.finally(() => setBusy(null));
|
||||
},
|
||||
[reload, log],
|
||||
);
|
||||
|
||||
return { busy, setBusy, error, setError, notice, setNotice, toggle, del };
|
||||
return { busy, setBusy, error, setError, notice, setNotice, ...actions };
|
||||
}
|
||||
Reference in new issue
Block a user