Compare commits

..

No commits in common. "main" and "test" have entirely different histories.
main ... test

12 changed files with 12 additions and 261 deletions

View File

@ -4556,27 +4556,6 @@
]
}
},
"/integrations/withdrawal-applications": {
"post": {
"operationId": "createExternalWithdrawalApplication",
"description": "来源钱包使用 App 独立 Bearer token 幂等提交提现申请,不使用后台用户 JWT。",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ExternalWithdrawalApplicationInput"
}
}
}
},
"responses": {
"201": {
"$ref": "#/components/responses/FinanceWithdrawalApplicationResponse"
}
}
}
},
"/admin/finance/withdrawal-applications": {
"get": {
"operationId": "listFinanceWithdrawalApplications",
@ -14309,14 +14288,6 @@
"appName": {
"type": "string"
},
"sourceSystem": {
"type": "string",
"description": "资金执行来源hyapp_wallet 为平台钱包likei 为 Aslan legacy 钱包"
},
"sourceApplicationId": {
"type": "string",
"description": "来源钱包的提现申请号,用于跨系统审计和幂等回调"
},
"userId": {
"type": "string"
},
@ -14391,32 +14362,6 @@
}
}
},
"ExternalWithdrawalApplicationInput": {
"type": "object",
"required": [
"appCode",
"sourceSystem",
"sourceApplicationId",
"userId",
"salaryAssetType",
"withdrawAmount",
"withdrawAmountMinor",
"withdrawMethod",
"withdrawAddress"
],
"properties": {
"appCode": { "type": "string" },
"sourceSystem": { "type": "string" },
"sourceApplicationId": { "type": "string" },
"userId": { "type": "string" },
"salaryAssetType": { "type": "string" },
"withdrawAmount": { "type": "string" },
"withdrawAmountMinor": { "type": "integer", "minimum": 1 },
"withdrawMethod": { "type": "string" },
"withdrawAddress": { "type": "string" },
"createdAtMs": { "type": "integer", "format": "int64" }
}
},
"FinanceCoinSellerRechargeOrderInput": {
"type": "object",
"required": ["appCode", "targetUserId", "usdAmount", "providerCode", "externalOrderNo"],

View File

@ -73,7 +73,6 @@ export function FinanceWithdrawalApplicationList({
<TableHead>
<TableRow>
<TableCell>APP</TableCell>
<TableCell>来源单号</TableCell>
<TableCell>用户ID</TableCell>
<TableCell align="right">提现金额</TableCell>
<TableCell>提现方式</TableCell>
@ -108,12 +107,6 @@ export function FinanceWithdrawalApplicationList({
size="small"
/>
</TableCell>
<TableCell>
<Stacked
primary={sourceSystemLabel(item.sourceSystem)}
secondary={item.sourceApplicationId || ""}
/>
</TableCell>
<TableCell>{item.userId || "-"}</TableCell>
<TableCell align="right">$ {formatAmount(item.withdrawAmount)}</TableCell>
<TableCell>{item.withdrawMethod || "-"}</TableCell>
@ -221,7 +214,7 @@ export function FinanceWithdrawalApplicationList({
))
) : (
<TableRow>
<TableCell align="center" colSpan={isOperationsReview ? 11 : 12}>
<TableCell align="center" colSpan={isOperationsReview ? 10 : 11}>
当前无数据
</TableCell>
</TableRow>
@ -247,16 +240,6 @@ export function FinanceWithdrawalApplicationList({
);
}
function sourceSystemLabel(value) {
if (!value || value === "hyapp_wallet") {
return "HY 钱包";
}
if (value === "likei") {
return "Likei";
}
return value;
}
function ReviewContext({ application }) {
return (
<span className="finance-review-context">

View File

@ -50,8 +50,6 @@ test("renders withdrawal application table with requested finance columns", () =
operationsReviewerName: "运营一",
operationsReviewerUserId: 11,
operationsStatus: "approved",
sourceApplicationId: "991001",
sourceSystem: "likei",
status: "approved",
userId: "10001",
withdrawAddress: "TRX-address",
@ -64,7 +62,6 @@ test("renders withdrawal application table with requested finance columns", () =
});
expect(screen.getByRole("columnheader", { name: "APP" })).toBeInTheDocument();
expect(screen.getByRole("columnheader", { name: "来源单号" })).toBeInTheDocument();
expect(screen.getByRole("columnheader", { name: "用户ID" })).toBeInTheDocument();
expect(screen.getByRole("columnheader", { name: "提现金额" })).toBeInTheDocument();
expect(screen.getByRole("columnheader", { name: "提现方式" })).toBeInTheDocument();
@ -76,8 +73,6 @@ test("renders withdrawal application table with requested finance columns", () =
expect(screen.getByRole("columnheader", { name: "财务审核原因" })).toBeInTheDocument();
expect(screen.getByRole("columnheader", { name: "操作" })).toBeInTheDocument();
expect(screen.getByText("lalu")).toBeInTheDocument();
expect(screen.getByText("Likei")).toBeInTheDocument();
expect(screen.getByText("991001")).toBeInTheDocument();
expect(screen.getByText("10001")).toBeInTheDocument();
expect(screen.getByText("$ 12.5")).toBeInTheDocument();
expect(screen.getByText("USDT-TRC20")).toBeInTheDocument();
@ -93,7 +88,7 @@ test("renders withdrawal application table with requested finance columns", () =
test("keeps empty withdrawal table colspan aligned with visible columns", () => {
const { container } = renderList(baseProps);
expect(container.querySelector("tbody td")?.colSpan).toBe(12);
expect(container.querySelector("tbody td")?.colSpan).toBe(11);
expect(screen.getByText("当前无数据")).toBeInTheDocument();
});

View File

@ -31,8 +31,6 @@ export function normalizeWithdrawalApplication(item = {}) {
operationsReviewerUserId: item.operationsReviewerUserId ?? item.operations_reviewer_user_id,
operationsStatus: stringValue(item.operationsStatus ?? item.operations_status),
salaryAssetType: stringValue(item.salaryAssetType ?? item.salary_asset_type),
sourceApplicationId: stringValue(item.sourceApplicationId ?? item.source_application_id),
sourceSystem: stringValue(item.sourceSystem ?? item.source_system),
status: stringValue(item.status),
updatedAtMs: numberOrNull(item.updatedAtMs ?? item.updated_at_ms),
userId: stringValue(item.userId ?? item.user_id),

View File

@ -520,8 +520,8 @@ export function OpsCenterApp({ can = denyPermission }) {
<UserProfilesView
apps={data.apps}
defaultAppCode={defaultAppCode}
key={profileRefreshKey}
poolIDsByApp={poolIDsByApp}
refreshKey={profileRefreshKey}
/>
</Suspense>
) : null}

View File

@ -9,8 +9,6 @@ import {
fetchLuckyGiftDraws,
fetchLuckyGiftConfigVersions,
fetchLuckyGiftExperiments,
fetchLuckyGiftUserProfile,
fetchLuckyGiftUserProfiles,
fetchOpsDashboard,
rollbackLuckyGiftConfig,
saveLuckyGiftConfig,
@ -27,8 +25,6 @@ vi.mock("./api.js", () => ({
fetchLuckyGiftDraws: vi.fn(),
fetchLuckyGiftConfigVersions: vi.fn(),
fetchLuckyGiftExperiments: vi.fn(),
fetchLuckyGiftUserProfile: vi.fn(),
fetchLuckyGiftUserProfiles: vi.fn(),
fetchOpsDashboard: vi.fn(),
luckyGiftPoolKey: (item = {}) =>
`${item.app_code || item.appCode}:${item.pool_id || item.poolId}:${item.strategy_version || item.strategyVersion || "fixed_v2"}`,
@ -87,16 +83,6 @@ beforeEach(() => {
pageSize: 20,
total: 1,
});
fetchLuckyGiftUserProfile.mockResolvedValue({});
fetchLuckyGiftUserProfiles.mockResolvedValue({
hasMore: false,
items: [],
nextCursor: "",
page: 1,
pageSize: 20,
snapshotAtMs: 1767000000000,
total: 0,
});
fetchOpsDashboard.mockResolvedValue({
experiments: [mockRunningExperiment()],
giftHostDiamondRatiosByApp: {
@ -311,34 +297,6 @@ test("opens the requested config view from the legacy admin document redirect",
expect(screen.getAllByText("lucky").length).toBeGreaterThan(0);
});
test("refreshes user profiles with the active filters instead of resetting the view", async () => {
renderApp();
await screen.findByText("运营应用");
fireEvent.click(screen.getByRole("button", { name: /用户画像/ }));
await waitFor(() => expect(fetchLuckyGiftUserProfiles).toHaveBeenCalledTimes(1));
fireEvent.mouseDown(screen.getByLabelText("用户来源"));
fireEvent.click(await screen.findByRole("option", { name: "内部用户" }));
const keywordInput = screen.getByPlaceholderText("短号 / 长号 / 靓号");
fireEvent.change(keywordInput, { target: { value: "163001" } });
fireEvent.click(screen.getByRole("button", { name: "查询" }));
await waitFor(() =>
expect(fetchLuckyGiftUserProfiles).toHaveBeenLastCalledWith(
expect.objectContaining({ identityType: "internal", keyword: "163001" }),
),
);
const requestCountBeforeRefresh = fetchLuckyGiftUserProfiles.mock.calls.length;
fireEvent.click(screen.getByRole("button", { name: "刷新" }));
await waitFor(() => expect(fetchLuckyGiftUserProfiles).toHaveBeenCalledTimes(requestCountBeforeRefresh + 1));
expect(fetchLuckyGiftUserProfiles).toHaveBeenLastCalledWith(
expect.objectContaining({ identityType: "internal", keyword: "163001", page: 1 }),
);
expect(screen.getByLabelText("用户来源")).toHaveTextContent("内部用户");
expect(keywordInput).toHaveValue("163001");
});
test("lists every pool config including published non-default pools", async () => {
renderApp();
await screen.findByText("运营应用");

View File

@ -44,7 +44,7 @@ const SORT_DIRECTION_OPTIONS = [
["asc", "从低到高 / 从旧到新"],
];
export function UserProfilesView({ apps, defaultAppCode, poolIDsByApp, refreshKey = 0 }) {
export function UserProfilesView({ apps, defaultAppCode, poolIDsByApp }) {
const [appCode, setAppCode] = useState(defaultAppCode);
const [poolId, setPoolId] = useState("");
const [identityType, setIdentityType] = useState("all");
@ -58,7 +58,6 @@ export function UserProfilesView({ apps, defaultAppCode, poolIDsByApp, refreshKe
const [reloadKey, setReloadKey] = useState(0);
const [expandedKey, setExpandedKey] = useState("");
const [details, setDetails] = useState({});
const handledRefreshKey = useRef(refreshKey);
// DataTable 使 owner cursor
// OFFSET
const cursorsByPage = useRef({ 1: "" });
@ -168,16 +167,6 @@ export function UserProfilesView({ apps, defaultAppCode, poolIDsByApp, refreshKe
setReloadKey((current) => current + 1);
}, []);
useEffect(() => {
if (handledRefreshKey.current === refreshKey) {
return;
}
handledRefreshKey.current = refreshKey;
//
// React key
reloadProfiles();
}, [refreshKey, reloadProfiles]);
const changeAppCode = useCallback(
(nextAppCode) => {
setAppCode(nextAppCode);

View File

@ -1036,21 +1036,11 @@ function ResourceFormDialog({ disabled, form, loading, mode, onClose, onSubmit,
const handleAnimationSelected = async (file) => {
let detectedMp4Layout = null;
if (isMp4File(file)) {
try {
detectedMp4Layout = await detectMp4AlphaLayout(file);
} catch {
// MP4
detectedMp4Layout = null;
}
detectedMp4Layout = await detectMp4AlphaLayout(file);
}
let detectedProfileCardLayout = null;
if (form.resourceType === "profile_card") {
try {
detectedProfileCardLayout = await detectProfileCardLayout(file);
} catch {
// PAG libpag
detectedProfileCardLayout = null;
}
detectedProfileCardLayout = await detectProfileCardLayout(file);
}
//
return {

View File

@ -54,7 +54,6 @@ export const API_OPERATIONS = {
createEmojiPack: "createEmojiPack",
createExploreTab: "createExploreTab",
createExternalAdminUser: "createExternalAdminUser",
createExternalWithdrawalApplication: "createExternalWithdrawalApplication",
createFinanceCoinSellerRechargeOrder: "createFinanceCoinSellerRechargeOrder",
createFullServerNoticeFanout: "createFullServerNoticeFanout",
createGift: "createGift",
@ -713,11 +712,6 @@ export const API_ENDPOINTS: Record<ApiOperationId, ApiEndpoint> = {
permission: "external-admin-user:create",
permissions: ["external-admin-user:create","external-admin-user:permissions"]
},
createExternalWithdrawalApplication: {
method: "POST",
operationId: API_OPERATIONS.createExternalWithdrawalApplication,
path: "/v1/integrations/withdrawal-applications"
},
createFinanceCoinSellerRechargeOrder: {
method: "POST",
operationId: API_OPERATIONS.createFinanceCoinSellerRechargeOrder,

View File

@ -2636,23 +2636,6 @@ export interface paths {
patch?: never;
trace?: never;
};
"/integrations/withdrawal-applications": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/** @description 来源钱包使用 App 独立 Bearer token 幂等提交提现申请,不使用后台用户 JWT。 */
post: operations["createExternalWithdrawalApplication"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/admin/finance/withdrawal-applications": {
parameters: {
query?: never;
@ -6669,10 +6652,6 @@ export interface components {
id: number;
appCode: string;
appName?: string;
/** @description 资金执行来源hyapp_wallet 为平台钱包likei 为 Aslan legacy 钱包 */
sourceSystem?: string;
/** @description 来源钱包的提现申请号,用于跨系统审计和幂等回调 */
sourceApplicationId?: string;
userId: string;
salaryAssetType?: string;
withdrawAmount: string;
@ -6700,19 +6679,6 @@ export interface components {
createdAtMs: number;
updatedAtMs?: number;
};
ExternalWithdrawalApplicationInput: {
appCode: string;
sourceSystem: string;
sourceApplicationId: string;
userId: string;
salaryAssetType: string;
withdrawAmount: string;
withdrawAmountMinor: number;
withdrawMethod: string;
withdrawAddress: string;
/** Format: int64 */
createdAtMs?: number;
};
FinanceCoinSellerRechargeOrderInput: {
appCode: string;
targetUserId: string;
@ -11516,22 +11482,6 @@ export interface operations {
200: components["responses"]["FinanceScopeCatalogResponse"];
};
};
createExternalWithdrawalApplication: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["ExternalWithdrawalApplicationInput"];
};
};
responses: {
201: components["responses"]["FinanceWithdrawalApplicationResponse"];
};
};
listFinanceWithdrawalApplications: {
parameters: {
query?: {

View File

@ -19,6 +19,7 @@ import { Mp4Preview } from "@/shared/ui/Mp4Preview.jsx";
import { useToast } from "@/shared/ui/ToastProvider.jsx";
import styles from "./UploadField.module.css";
const imageAccept = "image/jpeg,image/png,image/webp,.jpg,.jpeg,.png,.webp,.svga,.pag";
const pagWasmURL = "/vendor/libpag.wasm";
let pagModulePromise;
@ -50,10 +51,12 @@ export function UploadField({
const [downloading, setDownloading] = useState(false);
const [videoPreviewOpen, setVideoPreviewOpen] = useState(false);
const isImage = kind === "image";
const uploadMode = uploadKind || kind;
const useImageUpload = uploadMode === "image";
const source = localPreview?.url || value;
const assetKind = localPreview?.assetKind || getAssetKind(value, kind);
const displayName = localPreview?.name || getDisplayValue(value);
const inputAccept = accept || undefined;
const inputAccept = accept !== undefined ? accept : useImageUpload && isImage ? imageAccept : undefined;
const canPreviewVideo = Boolean(source && assetKind === "mp4");
const localPreviewURL = localPreview?.url;
const previewMp4Layout = Object.prototype.hasOwnProperty.call(localPreview?.selectionResult || {}, "mp4Layout")
@ -112,9 +115,7 @@ export function UploadField({
return;
}
setLocalPreview((previous) => (previous ? { ...previous, selectionResult } : previous));
//
// JPEG/PNG/WebP PAG/SVGA/GIF/AVIF
const result = shouldUploadAsImage(file, kind, uploadKind) ? await uploadImage(file) : await uploadFile(file);
const result = useImageUpload ? await uploadImage(file) : await uploadFile(file);
if (uploadGeneration !== uploadGenerationRef.current) {
return;
}
@ -549,20 +550,6 @@ function getAssetKind(value, kind, contentType = "") {
return kind === "image" ? "image" : "file";
}
function shouldUploadAsImage(file, kind, uploadKind) {
if (uploadKind !== undefined) {
return uploadKind === "image";
}
if (kind !== "image") {
return false;
}
const extension = getExtension(file?.name);
if (extension) {
return ["jpg", "jpeg", "png", "webp"].includes(extension);
}
return ["image/jpeg", "image/png", "image/webp"].includes(String(file?.type || "").toLowerCase());
}
function isRasterImageExtension(extension) {
return ["avif", "gif", "jpg", "jpeg", "png", "webp"].includes(extension);
}

View File

@ -1,5 +1,5 @@
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { beforeEach, expect, test, vi } from "vitest";
import { expect, test, vi } from "vitest";
import { downloadResponse } from "@/shared/api/download";
import { uploadFile, uploadImage } from "@/shared/api/upload";
import { ToastProvider } from "@/shared/ui/ToastProvider.jsx";
@ -14,10 +14,6 @@ vi.mock("@/shared/api/download", () => ({
downloadResponse: vi.fn(async () => undefined),
}));
beforeEach(() => {
vi.clearAllMocks();
});
test("file upload field does not restrict picker type and uses generic upload endpoint", async () => {
const onChange = vi.fn();
const onFileSelected = vi.fn();
@ -42,40 +38,6 @@ test("file upload field does not restrict picker type and uses generic upload en
expect(onChange).toHaveBeenCalledWith("https://media.haiyihy.com/admin/files/effect.bin");
});
test("image-style resource field accepts PAG and routes it through generic upload", async () => {
const onChange = vi.fn();
const { container } = render(
<ToastProvider>
<UploadField kind="image" label="动效素材" value="" onChange={onChange} />
</ToastProvider>,
);
const input = container.querySelector('input[type="file"]');
const file = new File(["pag"], "effect.pag", { type: "application/octet-stream" });
expect(input).not.toHaveAttribute("accept");
fireEvent.change(input, { target: { files: [file] } });
await waitFor(() => expect(uploadFile).toHaveBeenCalledWith(file));
expect(uploadImage).not.toHaveBeenCalled();
expect(onChange).toHaveBeenCalledWith("https://media.haiyihy.com/admin/files/effect.bin");
});
test("standard raster image keeps the strict image upload endpoint", async () => {
const onChange = vi.fn();
const { container } = render(
<ToastProvider>
<UploadField kind="image" label="封面" value="" onChange={onChange} />
</ToastProvider>,
);
const file = new File(["png"], "cover.png", { type: "image/png" });
fireEvent.change(container.querySelector('input[type="file"]'), { target: { files: [file] } });
await waitFor(() => expect(uploadImage).toHaveBeenCalledWith(file));
expect(uploadFile).not.toHaveBeenCalled();
expect(onChange).toHaveBeenCalledWith("https://media.haiyihy.com/admin/images/effect.png");
});
test("mp4 file upload field can open video preview dialog", async () => {
const source = "https://media.haiyihy.com/admin/files/gift.mp4?sign=1";
const onChange = vi.fn();