用户靓号收回
This commit is contained in:
parent
b81c2edf43
commit
810ee6721c
@ -4802,6 +4802,43 @@
|
||||
"x-permissions": ["pretty-id:grant"]
|
||||
}
|
||||
},
|
||||
"/admin/users/pretty-ids/{pretty_id}/recycle": {
|
||||
"post": {
|
||||
"operationId": "recyclePrettyId",
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "#/components/responses/EmptyResponse"
|
||||
}
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"name": "pretty_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": false,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"reason": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"x-permission": "pretty-id:update",
|
||||
"x-permissions": ["pretty-id:update"]
|
||||
}
|
||||
},
|
||||
"/admin/users/pretty-ids/{pretty_id}/status": {
|
||||
"post": {
|
||||
"operationId": "setPrettyIdStatus",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { afterEach, expect, test, vi } from "vitest";
|
||||
import { setAccessToken } from "@/shared/api/request";
|
||||
import { getAppUser, listAppUsers, listPrettyDisplayIDs } from "./api";
|
||||
import { getAppUser, listAppUsers, listPrettyDisplayIDs, recyclePrettyDisplayID } from "./api";
|
||||
|
||||
afterEach(() => {
|
||||
setAccessToken("");
|
||||
@ -122,6 +122,40 @@ test("listPrettyDisplayIDs normalizes assigned user and operator snapshots", asy
|
||||
});
|
||||
});
|
||||
|
||||
test("recyclePrettyDisplayID uses generated recycle endpoint", async () => {
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn(
|
||||
async () =>
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
code: 0,
|
||||
data: {
|
||||
assigned_user_id: "",
|
||||
display_user_id: "777777",
|
||||
pretty_id: "pretty-2",
|
||||
released_at_ms: 1782493200000,
|
||||
source: "admin_grant",
|
||||
status: "released",
|
||||
},
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
const result = await recyclePrettyDisplayID("pretty-2");
|
||||
const [url, init] = vi.mocked(fetch).mock.calls[0];
|
||||
|
||||
expect(String(url)).toContain("/api/v1/admin/users/pretty-ids/pretty-2/recycle");
|
||||
expect(init?.method).toBe("POST");
|
||||
expect(result).toMatchObject({
|
||||
assignedUserId: "",
|
||||
displayUserId: "777777",
|
||||
prettyId: "pretty-2",
|
||||
status: "released",
|
||||
});
|
||||
});
|
||||
|
||||
test("listAppUsers sends country region and time filters", async () => {
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
|
||||
@ -16,6 +16,7 @@ import type {
|
||||
PrettyDisplayIDGenerationBatchDto,
|
||||
PrettyDisplayIDPoolDto,
|
||||
PrettyDisplayIDPoolPayload,
|
||||
RecyclePrettyDisplayIDPayload,
|
||||
SetPrettyDisplayIDStatusPayload,
|
||||
} from "@/shared/api/types";
|
||||
|
||||
@ -180,6 +181,21 @@ export async function grantPrettyDisplayID(
|
||||
return normalizeAdminGrantPrettyDisplayIDResult(data);
|
||||
}
|
||||
|
||||
export async function recyclePrettyDisplayID(
|
||||
prettyId: EntityId,
|
||||
payload: RecyclePrettyDisplayIDPayload = {},
|
||||
): Promise<PrettyDisplayIDDto> {
|
||||
const endpoint = API_ENDPOINTS.recyclePrettyId;
|
||||
const data = await apiRequest<RawPrettyDisplayID, RecyclePrettyDisplayIDPayload>(
|
||||
apiEndpointPath(API_OPERATIONS.recyclePrettyId, { pretty_id: prettyId }),
|
||||
{
|
||||
body: payload,
|
||||
method: endpoint.method,
|
||||
},
|
||||
);
|
||||
return normalizePrettyDisplayID(data);
|
||||
}
|
||||
|
||||
export async function setPrettyDisplayIDStatus(
|
||||
prettyId: EntityId,
|
||||
payload: SetPrettyDisplayIDStatusPayload,
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { parseForm } from "@/shared/forms/validation";
|
||||
import { usePaginatedQuery } from "@/shared/hooks/usePaginatedQuery.js";
|
||||
import { useConfirm } from "@/shared/ui/ConfirmProvider.jsx";
|
||||
import { useToast } from "@/shared/ui/ToastProvider.jsx";
|
||||
import {
|
||||
createPrettyDisplayIDPool,
|
||||
@ -8,6 +9,7 @@ import {
|
||||
grantPrettyDisplayID,
|
||||
listPrettyDisplayIDPools,
|
||||
listPrettyDisplayIDs,
|
||||
recyclePrettyDisplayID,
|
||||
setPrettyDisplayIDStatus,
|
||||
updatePrettyDisplayIDPool,
|
||||
} from "@/features/app-users/api";
|
||||
@ -36,6 +38,7 @@ const emptyStatusForm = () => ({ prettyId: "", reason: "", status: "available" }
|
||||
|
||||
export function usePrettyIdsPage() {
|
||||
const abilities = useAppUserAbilities();
|
||||
const confirm = useConfirm();
|
||||
const { showToast } = useToast();
|
||||
const [poolPage, setPoolPage] = useState(1);
|
||||
const [poolStatus, setPoolStatus] = useState("");
|
||||
@ -230,6 +233,26 @@ export function usePrettyIdsPage() {
|
||||
});
|
||||
};
|
||||
|
||||
const recyclePrettyID = async (item) => {
|
||||
if (!abilities.canPrettyUpdate || !canRecyclePrettyID(item)) {
|
||||
return;
|
||||
}
|
||||
const accepted = await confirm({
|
||||
title: "回收靓号",
|
||||
message: `确认回收靓号 ${item.displayUserId || item.prettyId}?回收后用户将恢复默认短号。`,
|
||||
confirmText: "回收",
|
||||
tone: "danger",
|
||||
});
|
||||
if (!accepted) {
|
||||
return;
|
||||
}
|
||||
await runAction(`recycle-${item.prettyId}`, "靓号已回收", async () => {
|
||||
// 回收只交给后台管理回收接口处理,前端不直接改 assigned/released 字段,避免和来源回收规则分叉。
|
||||
await recyclePrettyDisplayID(item.prettyId);
|
||||
await idQuery.reload();
|
||||
});
|
||||
};
|
||||
|
||||
const changePoolStatus = (value) => {
|
||||
setPoolStatus(value);
|
||||
// 任意筛选变化都回到第一页,避免旧页码在新条件下落到空页。
|
||||
@ -333,6 +356,7 @@ export function usePrettyIdsPage() {
|
||||
reloadAll,
|
||||
resetIDFilters,
|
||||
resetPoolFilters,
|
||||
recyclePrettyID,
|
||||
setActiveTab,
|
||||
setGenerateForm,
|
||||
setGrantForm,
|
||||
@ -347,3 +371,10 @@ export function usePrettyIdsPage() {
|
||||
submitStatus,
|
||||
};
|
||||
}
|
||||
|
||||
function canRecyclePrettyID(item) {
|
||||
const assigned = item?.assignedUserId;
|
||||
const hasAssignedUser =
|
||||
assigned !== 0 && assigned !== "0" && assigned !== "" && assigned !== undefined && assigned !== null;
|
||||
return Boolean(item?.prettyId) && item.status === "assigned" && hasAssignedUser;
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import AddCircleOutlineOutlined from "@mui/icons-material/AddCircleOutlineOutlined";
|
||||
import AssignmentReturnOutlined from "@mui/icons-material/AssignmentReturnOutlined";
|
||||
import AutoAwesomeOutlined from "@mui/icons-material/AutoAwesomeOutlined";
|
||||
import EditOutlined from "@mui/icons-material/EditOutlined";
|
||||
import RestartAltOutlined from "@mui/icons-material/RestartAltOutlined";
|
||||
@ -365,16 +366,32 @@ function idColumns(page) {
|
||||
key: "actions",
|
||||
label: "操作",
|
||||
width: "minmax(96px, 0.5fr)",
|
||||
render: (item) =>
|
||||
page.abilities.canPrettyUpdate && canChangePrettyStatus(item) ? (
|
||||
render: (item) => {
|
||||
const showStatus = page.abilities.canPrettyUpdate && canChangePrettyStatus(item);
|
||||
const showRecycle = page.abilities.canPrettyUpdate && canRecyclePrettyID(item);
|
||||
if (!showStatus && !showRecycle) {
|
||||
return "-";
|
||||
}
|
||||
return (
|
||||
<div className={styles.rowActions}>
|
||||
<ActionIcon label="状态" onClick={() => page.openStatus(item)}>
|
||||
<SettingsOutlined fontSize="small" />
|
||||
</ActionIcon>
|
||||
{showStatus ? (
|
||||
<ActionIcon label="状态" onClick={() => page.openStatus(item)}>
|
||||
<SettingsOutlined fontSize="small" />
|
||||
</ActionIcon>
|
||||
) : null}
|
||||
{showRecycle ? (
|
||||
<ActionIcon
|
||||
disabled={page.loadingAction === `recycle-${item.prettyId}`}
|
||||
label="回收"
|
||||
tone="danger"
|
||||
onClick={() => page.recyclePrettyID(item)}
|
||||
>
|
||||
<AssignmentReturnOutlined fontSize="small" />
|
||||
</ActionIcon>
|
||||
) : null}
|
||||
</div>
|
||||
) : (
|
||||
"-"
|
||||
),
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
@ -654,11 +671,11 @@ function StatusDialog({ page }) {
|
||||
);
|
||||
}
|
||||
|
||||
function ActionIcon({ children, label, onClick }) {
|
||||
function ActionIcon({ children, label, ...props }) {
|
||||
return (
|
||||
<Tooltip arrow title={label}>
|
||||
<span>
|
||||
<IconButton label={label} onClick={onClick}>
|
||||
<IconButton label={label} {...props}>
|
||||
{children}
|
||||
</IconButton>
|
||||
</span>
|
||||
@ -709,3 +726,10 @@ function canChangePrettyStatus(item) {
|
||||
assigned === 0 || assigned === "0" || assigned === "" || assigned === undefined || assigned === null;
|
||||
return ["available", "disabled", "reserved"].includes(item.status) && unassigned;
|
||||
}
|
||||
|
||||
function canRecyclePrettyID(item) {
|
||||
const assigned = item.assignedUserId;
|
||||
const hasAssignedUser =
|
||||
assigned !== 0 && assigned !== "0" && assigned !== "" && assigned !== undefined && assigned !== null;
|
||||
return item.status === "assigned" && hasAssignedUser;
|
||||
}
|
||||
|
||||
@ -23,6 +23,7 @@ test("pretty id page uses tabs and hides internal pretty record ids", () => {
|
||||
expect(screen.getByText("占用人 A")).toBeInTheDocument();
|
||||
expect(screen.getByText("Admin ID 8")).toBeInTheDocument();
|
||||
expect(screen.getByText("释放 2026-06-27 01:00:00")).toBeInTheDocument();
|
||||
expect(screen.getAllByLabelText("回收").length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("pretty id page can switch to pool tab", () => {
|
||||
@ -107,6 +108,7 @@ function pageFixture(patch = {}) {
|
||||
reloadAll: vi.fn(),
|
||||
resetIDFilters: vi.fn(),
|
||||
resetPoolFilters: vi.fn(),
|
||||
recyclePrettyID: vi.fn(),
|
||||
setActiveTab: vi.fn(),
|
||||
setGenerateForm: vi.fn(),
|
||||
setGrantForm: vi.fn(),
|
||||
|
||||
@ -221,6 +221,7 @@ export const API_OPERATIONS = {
|
||||
me: "me",
|
||||
navigationMenus: "navigationMenus",
|
||||
publishHostAgencyPolicy: "publishHostAgencyPolicy",
|
||||
recyclePrettyId: "recyclePrettyId",
|
||||
refresh: "refresh",
|
||||
replaceCoinSellerSalaryRates: "replaceCoinSellerSalaryRates",
|
||||
replaceRegionBlocks: "replaceRegionBlocks",
|
||||
@ -1757,6 +1758,13 @@ export const API_ENDPOINTS: Record<ApiOperationId, ApiEndpoint> = {
|
||||
permission: "host-agency-policy:publish",
|
||||
permissions: ["host-agency-policy:publish"]
|
||||
},
|
||||
recyclePrettyId: {
|
||||
method: "POST",
|
||||
operationId: API_OPERATIONS.recyclePrettyId,
|
||||
path: "/v1/admin/users/pretty-ids/{pretty_id}/recycle",
|
||||
permission: "pretty-id:update",
|
||||
permissions: ["pretty-id:update"]
|
||||
},
|
||||
refresh: {
|
||||
method: "POST",
|
||||
operationId: API_OPERATIONS.refresh,
|
||||
|
||||
36
src/shared/api/generated/schema.d.ts
vendored
36
src/shared/api/generated/schema.d.ts
vendored
@ -3364,6 +3364,22 @@ export interface paths {
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/admin/users/pretty-ids/{pretty_id}/recycle": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
post: operations["recyclePrettyId"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/admin/users/pretty-ids/{pretty_id}/status": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
@ -7836,6 +7852,26 @@ export interface operations {
|
||||
200: components["responses"]["EmptyResponse"];
|
||||
};
|
||||
};
|
||||
recyclePrettyId: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
pretty_id: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: {
|
||||
content: {
|
||||
"application/json": {
|
||||
reason?: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
200: components["responses"]["EmptyResponse"];
|
||||
};
|
||||
};
|
||||
setPrettyIdStatus: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
|
||||
@ -1023,6 +1023,10 @@ export interface GrantPrettyDisplayIDPayload {
|
||||
target_user_id: EntityId;
|
||||
}
|
||||
|
||||
export interface RecyclePrettyDisplayIDPayload {
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
export interface SetPrettyDisplayIDStatusPayload {
|
||||
reason?: string;
|
||||
status: string;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user