Compare commits
No commits in common. "main" and "test" have entirely different histories.
@ -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": {
|
"/admin/finance/withdrawal-applications": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "listFinanceWithdrawalApplications",
|
"operationId": "listFinanceWithdrawalApplications",
|
||||||
@ -14309,14 +14288,6 @@
|
|||||||
"appName": {
|
"appName": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"sourceSystem": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "资金执行来源;hyapp_wallet 为平台钱包,likei 为 Aslan legacy 钱包"
|
|
||||||
},
|
|
||||||
"sourceApplicationId": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "来源钱包的提现申请号,用于跨系统审计和幂等回调"
|
|
||||||
},
|
|
||||||
"userId": {
|
"userId": {
|
||||||
"type": "string"
|
"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": {
|
"FinanceCoinSellerRechargeOrderInput": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": ["appCode", "targetUserId", "usdAmount", "providerCode", "externalOrderNo"],
|
"required": ["appCode", "targetUserId", "usdAmount", "providerCode", "externalOrderNo"],
|
||||||
|
|||||||
@ -73,7 +73,6 @@ export function FinanceWithdrawalApplicationList({
|
|||||||
<TableHead>
|
<TableHead>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell>APP</TableCell>
|
<TableCell>APP</TableCell>
|
||||||
<TableCell>来源单号</TableCell>
|
|
||||||
<TableCell>用户ID</TableCell>
|
<TableCell>用户ID</TableCell>
|
||||||
<TableCell align="right">提现金额</TableCell>
|
<TableCell align="right">提现金额</TableCell>
|
||||||
<TableCell>提现方式</TableCell>
|
<TableCell>提现方式</TableCell>
|
||||||
@ -108,12 +107,6 @@ export function FinanceWithdrawalApplicationList({
|
|||||||
size="small"
|
size="small"
|
||||||
/>
|
/>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
|
||||||
<Stacked
|
|
||||||
primary={sourceSystemLabel(item.sourceSystem)}
|
|
||||||
secondary={item.sourceApplicationId || ""}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>{item.userId || "-"}</TableCell>
|
<TableCell>{item.userId || "-"}</TableCell>
|
||||||
<TableCell align="right">$ {formatAmount(item.withdrawAmount)}</TableCell>
|
<TableCell align="right">$ {formatAmount(item.withdrawAmount)}</TableCell>
|
||||||
<TableCell>{item.withdrawMethod || "-"}</TableCell>
|
<TableCell>{item.withdrawMethod || "-"}</TableCell>
|
||||||
@ -221,7 +214,7 @@ export function FinanceWithdrawalApplicationList({
|
|||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell align="center" colSpan={isOperationsReview ? 11 : 12}>
|
<TableCell align="center" colSpan={isOperationsReview ? 10 : 11}>
|
||||||
当前无数据
|
当前无数据
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</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 }) {
|
function ReviewContext({ application }) {
|
||||||
return (
|
return (
|
||||||
<span className="finance-review-context">
|
<span className="finance-review-context">
|
||||||
|
|||||||
@ -50,8 +50,6 @@ test("renders withdrawal application table with requested finance columns", () =
|
|||||||
operationsReviewerName: "运营一",
|
operationsReviewerName: "运营一",
|
||||||
operationsReviewerUserId: 11,
|
operationsReviewerUserId: 11,
|
||||||
operationsStatus: "approved",
|
operationsStatus: "approved",
|
||||||
sourceApplicationId: "991001",
|
|
||||||
sourceSystem: "likei",
|
|
||||||
status: "approved",
|
status: "approved",
|
||||||
userId: "10001",
|
userId: "10001",
|
||||||
withdrawAddress: "TRX-address",
|
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: "APP" })).toBeInTheDocument();
|
||||||
expect(screen.getByRole("columnheader", { name: "来源单号" })).toBeInTheDocument();
|
|
||||||
expect(screen.getByRole("columnheader", { name: "用户ID" })).toBeInTheDocument();
|
expect(screen.getByRole("columnheader", { name: "用户ID" })).toBeInTheDocument();
|
||||||
expect(screen.getByRole("columnheader", { name: "提现金额" })).toBeInTheDocument();
|
expect(screen.getByRole("columnheader", { name: "提现金额" })).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.getByRole("columnheader", { name: "操作" })).toBeInTheDocument();
|
expect(screen.getByRole("columnheader", { name: "操作" })).toBeInTheDocument();
|
||||||
expect(screen.getByText("lalu")).toBeInTheDocument();
|
expect(screen.getByText("lalu")).toBeInTheDocument();
|
||||||
expect(screen.getByText("Likei")).toBeInTheDocument();
|
|
||||||
expect(screen.getByText("991001")).toBeInTheDocument();
|
|
||||||
expect(screen.getByText("10001")).toBeInTheDocument();
|
expect(screen.getByText("10001")).toBeInTheDocument();
|
||||||
expect(screen.getByText("$ 12.5")).toBeInTheDocument();
|
expect(screen.getByText("$ 12.5")).toBeInTheDocument();
|
||||||
expect(screen.getByText("USDT-TRC20")).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", () => {
|
test("keeps empty withdrawal table colspan aligned with visible columns", () => {
|
||||||
const { container } = renderList(baseProps);
|
const { container } = renderList(baseProps);
|
||||||
|
|
||||||
expect(container.querySelector("tbody td")?.colSpan).toBe(12);
|
expect(container.querySelector("tbody td")?.colSpan).toBe(11);
|
||||||
expect(screen.getByText("当前无数据")).toBeInTheDocument();
|
expect(screen.getByText("当前无数据")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -31,8 +31,6 @@ export function normalizeWithdrawalApplication(item = {}) {
|
|||||||
operationsReviewerUserId: item.operationsReviewerUserId ?? item.operations_reviewer_user_id,
|
operationsReviewerUserId: item.operationsReviewerUserId ?? item.operations_reviewer_user_id,
|
||||||
operationsStatus: stringValue(item.operationsStatus ?? item.operations_status),
|
operationsStatus: stringValue(item.operationsStatus ?? item.operations_status),
|
||||||
salaryAssetType: stringValue(item.salaryAssetType ?? item.salary_asset_type),
|
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),
|
status: stringValue(item.status),
|
||||||
updatedAtMs: numberOrNull(item.updatedAtMs ?? item.updated_at_ms),
|
updatedAtMs: numberOrNull(item.updatedAtMs ?? item.updated_at_ms),
|
||||||
userId: stringValue(item.userId ?? item.user_id),
|
userId: stringValue(item.userId ?? item.user_id),
|
||||||
|
|||||||
@ -520,8 +520,8 @@ export function OpsCenterApp({ can = denyPermission }) {
|
|||||||
<UserProfilesView
|
<UserProfilesView
|
||||||
apps={data.apps}
|
apps={data.apps}
|
||||||
defaultAppCode={defaultAppCode}
|
defaultAppCode={defaultAppCode}
|
||||||
|
key={profileRefreshKey}
|
||||||
poolIDsByApp={poolIDsByApp}
|
poolIDsByApp={poolIDsByApp}
|
||||||
refreshKey={profileRefreshKey}
|
|
||||||
/>
|
/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@ -9,8 +9,6 @@ import {
|
|||||||
fetchLuckyGiftDraws,
|
fetchLuckyGiftDraws,
|
||||||
fetchLuckyGiftConfigVersions,
|
fetchLuckyGiftConfigVersions,
|
||||||
fetchLuckyGiftExperiments,
|
fetchLuckyGiftExperiments,
|
||||||
fetchLuckyGiftUserProfile,
|
|
||||||
fetchLuckyGiftUserProfiles,
|
|
||||||
fetchOpsDashboard,
|
fetchOpsDashboard,
|
||||||
rollbackLuckyGiftConfig,
|
rollbackLuckyGiftConfig,
|
||||||
saveLuckyGiftConfig,
|
saveLuckyGiftConfig,
|
||||||
@ -27,8 +25,6 @@ vi.mock("./api.js", () => ({
|
|||||||
fetchLuckyGiftDraws: vi.fn(),
|
fetchLuckyGiftDraws: vi.fn(),
|
||||||
fetchLuckyGiftConfigVersions: vi.fn(),
|
fetchLuckyGiftConfigVersions: vi.fn(),
|
||||||
fetchLuckyGiftExperiments: vi.fn(),
|
fetchLuckyGiftExperiments: vi.fn(),
|
||||||
fetchLuckyGiftUserProfile: vi.fn(),
|
|
||||||
fetchLuckyGiftUserProfiles: vi.fn(),
|
|
||||||
fetchOpsDashboard: vi.fn(),
|
fetchOpsDashboard: vi.fn(),
|
||||||
luckyGiftPoolKey: (item = {}) =>
|
luckyGiftPoolKey: (item = {}) =>
|
||||||
`${item.app_code || item.appCode}:${item.pool_id || item.poolId}:${item.strategy_version || item.strategyVersion || "fixed_v2"}`,
|
`${item.app_code || item.appCode}:${item.pool_id || item.poolId}:${item.strategy_version || item.strategyVersion || "fixed_v2"}`,
|
||||||
@ -87,16 +83,6 @@ beforeEach(() => {
|
|||||||
pageSize: 20,
|
pageSize: 20,
|
||||||
total: 1,
|
total: 1,
|
||||||
});
|
});
|
||||||
fetchLuckyGiftUserProfile.mockResolvedValue({});
|
|
||||||
fetchLuckyGiftUserProfiles.mockResolvedValue({
|
|
||||||
hasMore: false,
|
|
||||||
items: [],
|
|
||||||
nextCursor: "",
|
|
||||||
page: 1,
|
|
||||||
pageSize: 20,
|
|
||||||
snapshotAtMs: 1767000000000,
|
|
||||||
total: 0,
|
|
||||||
});
|
|
||||||
fetchOpsDashboard.mockResolvedValue({
|
fetchOpsDashboard.mockResolvedValue({
|
||||||
experiments: [mockRunningExperiment()],
|
experiments: [mockRunningExperiment()],
|
||||||
giftHostDiamondRatiosByApp: {
|
giftHostDiamondRatiosByApp: {
|
||||||
@ -311,34 +297,6 @@ test("opens the requested config view from the legacy admin document redirect",
|
|||||||
expect(screen.getAllByText("lucky").length).toBeGreaterThan(0);
|
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 () => {
|
test("lists every pool config including published non-default pools", async () => {
|
||||||
renderApp();
|
renderApp();
|
||||||
await screen.findByText("运营应用");
|
await screen.findByText("运营应用");
|
||||||
|
|||||||
@ -44,7 +44,7 @@ const SORT_DIRECTION_OPTIONS = [
|
|||||||
["asc", "从低到高 / 从旧到新"],
|
["asc", "从低到高 / 从旧到新"],
|
||||||
];
|
];
|
||||||
|
|
||||||
export function UserProfilesView({ apps, defaultAppCode, poolIDsByApp, refreshKey = 0 }) {
|
export function UserProfilesView({ apps, defaultAppCode, poolIDsByApp }) {
|
||||||
const [appCode, setAppCode] = useState(defaultAppCode);
|
const [appCode, setAppCode] = useState(defaultAppCode);
|
||||||
const [poolId, setPoolId] = useState("");
|
const [poolId, setPoolId] = useState("");
|
||||||
const [identityType, setIdentityType] = useState("all");
|
const [identityType, setIdentityType] = useState("all");
|
||||||
@ -58,7 +58,6 @@ export function UserProfilesView({ apps, defaultAppCode, poolIDsByApp, refreshKe
|
|||||||
const [reloadKey, setReloadKey] = useState(0);
|
const [reloadKey, setReloadKey] = useState(0);
|
||||||
const [expandedKey, setExpandedKey] = useState("");
|
const [expandedKey, setExpandedKey] = useState("");
|
||||||
const [details, setDetails] = useState({});
|
const [details, setDetails] = useState({});
|
||||||
const handledRefreshKey = useRef(refreshKey);
|
|
||||||
// 页码只负责兼容通用 DataTable 的滚动触发;真正翻页使用 owner 返回的不可解释 cursor,
|
// 页码只负责兼容通用 DataTable 的滚动触发;真正翻页使用 owner 返回的不可解释 cursor,
|
||||||
// 因此并发开奖插入新画像时不会因为 OFFSET 位移而把同一用户追加两次。
|
// 因此并发开奖插入新画像时不会因为 OFFSET 位移而把同一用户追加两次。
|
||||||
const cursorsByPage = useRef({ 1: "" });
|
const cursorsByPage = useRef({ 1: "" });
|
||||||
@ -168,16 +167,6 @@ export function UserProfilesView({ apps, defaultAppCode, poolIDsByApp, refreshKe
|
|||||||
setReloadKey((current) => current + 1);
|
setReloadKey((current) => current + 1);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (handledRefreshKey.current === refreshKey) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
handledRefreshKey.current = refreshKey;
|
|
||||||
// 顶部刷新只重建游标和当前结果,筛选与已提交关键词继续由本组件状态持有,
|
|
||||||
// 避免通过 React key 重挂载整页后把运营正在排查的条件恢复成默认值。
|
|
||||||
reloadProfiles();
|
|
||||||
}, [refreshKey, reloadProfiles]);
|
|
||||||
|
|
||||||
const changeAppCode = useCallback(
|
const changeAppCode = useCallback(
|
||||||
(nextAppCode) => {
|
(nextAppCode) => {
|
||||||
setAppCode(nextAppCode);
|
setAppCode(nextAppCode);
|
||||||
|
|||||||
@ -1036,21 +1036,11 @@ function ResourceFormDialog({ disabled, form, loading, mode, onClose, onSubmit,
|
|||||||
const handleAnimationSelected = async (file) => {
|
const handleAnimationSelected = async (file) => {
|
||||||
let detectedMp4Layout = null;
|
let detectedMp4Layout = null;
|
||||||
if (isMp4File(file)) {
|
if (isMp4File(file)) {
|
||||||
try {
|
|
||||||
detectedMp4Layout = await detectMp4AlphaLayout(file);
|
detectedMp4Layout = await detectMp4AlphaLayout(file);
|
||||||
} catch {
|
|
||||||
// 布局解析只负责补充客户端渲染元数据,失败不能把 MP4 变成不可上传格式。
|
|
||||||
detectedMp4Layout = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
let detectedProfileCardLayout = null;
|
let detectedProfileCardLayout = null;
|
||||||
if (form.resourceType === "profile_card") {
|
if (form.resourceType === "profile_card") {
|
||||||
try {
|
|
||||||
detectedProfileCardLayout = await detectProfileCardLayout(file);
|
detectedProfileCardLayout = await detectProfileCardLayout(file);
|
||||||
} catch {
|
|
||||||
// 部分 PAG 版本无法被浏览器侧 libpag 解码,但原文件仍可供客户端消费,继续走通用上传。
|
|
||||||
detectedProfileCardLayout = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// 只返回当次素材的检测增量,上传成功后再合并到最新表单,避免异步旧快照覆盖用户新修改。
|
// 只返回当次素材的检测增量,上传成功后再合并到最新表单,避免异步旧快照覆盖用户新修改。
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -54,7 +54,6 @@ export const API_OPERATIONS = {
|
|||||||
createEmojiPack: "createEmojiPack",
|
createEmojiPack: "createEmojiPack",
|
||||||
createExploreTab: "createExploreTab",
|
createExploreTab: "createExploreTab",
|
||||||
createExternalAdminUser: "createExternalAdminUser",
|
createExternalAdminUser: "createExternalAdminUser",
|
||||||
createExternalWithdrawalApplication: "createExternalWithdrawalApplication",
|
|
||||||
createFinanceCoinSellerRechargeOrder: "createFinanceCoinSellerRechargeOrder",
|
createFinanceCoinSellerRechargeOrder: "createFinanceCoinSellerRechargeOrder",
|
||||||
createFullServerNoticeFanout: "createFullServerNoticeFanout",
|
createFullServerNoticeFanout: "createFullServerNoticeFanout",
|
||||||
createGift: "createGift",
|
createGift: "createGift",
|
||||||
@ -713,11 +712,6 @@ export const API_ENDPOINTS: Record<ApiOperationId, ApiEndpoint> = {
|
|||||||
permission: "external-admin-user:create",
|
permission: "external-admin-user:create",
|
||||||
permissions: ["external-admin-user:create","external-admin-user:permissions"]
|
permissions: ["external-admin-user:create","external-admin-user:permissions"]
|
||||||
},
|
},
|
||||||
createExternalWithdrawalApplication: {
|
|
||||||
method: "POST",
|
|
||||||
operationId: API_OPERATIONS.createExternalWithdrawalApplication,
|
|
||||||
path: "/v1/integrations/withdrawal-applications"
|
|
||||||
},
|
|
||||||
createFinanceCoinSellerRechargeOrder: {
|
createFinanceCoinSellerRechargeOrder: {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
operationId: API_OPERATIONS.createFinanceCoinSellerRechargeOrder,
|
operationId: API_OPERATIONS.createFinanceCoinSellerRechargeOrder,
|
||||||
|
|||||||
50
src/shared/api/generated/schema.d.ts
vendored
50
src/shared/api/generated/schema.d.ts
vendored
@ -2636,23 +2636,6 @@ export interface paths {
|
|||||||
patch?: never;
|
patch?: never;
|
||||||
trace?: 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": {
|
"/admin/finance/withdrawal-applications": {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
@ -6669,10 +6652,6 @@ export interface components {
|
|||||||
id: number;
|
id: number;
|
||||||
appCode: string;
|
appCode: string;
|
||||||
appName?: string;
|
appName?: string;
|
||||||
/** @description 资金执行来源;hyapp_wallet 为平台钱包,likei 为 Aslan legacy 钱包 */
|
|
||||||
sourceSystem?: string;
|
|
||||||
/** @description 来源钱包的提现申请号,用于跨系统审计和幂等回调 */
|
|
||||||
sourceApplicationId?: string;
|
|
||||||
userId: string;
|
userId: string;
|
||||||
salaryAssetType?: string;
|
salaryAssetType?: string;
|
||||||
withdrawAmount: string;
|
withdrawAmount: string;
|
||||||
@ -6700,19 +6679,6 @@ export interface components {
|
|||||||
createdAtMs: number;
|
createdAtMs: number;
|
||||||
updatedAtMs?: 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: {
|
FinanceCoinSellerRechargeOrderInput: {
|
||||||
appCode: string;
|
appCode: string;
|
||||||
targetUserId: string;
|
targetUserId: string;
|
||||||
@ -11516,22 +11482,6 @@ export interface operations {
|
|||||||
200: components["responses"]["FinanceScopeCatalogResponse"];
|
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: {
|
listFinanceWithdrawalApplications: {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: {
|
query?: {
|
||||||
|
|||||||
@ -19,6 +19,7 @@ import { Mp4Preview } from "@/shared/ui/Mp4Preview.jsx";
|
|||||||
import { useToast } from "@/shared/ui/ToastProvider.jsx";
|
import { useToast } from "@/shared/ui/ToastProvider.jsx";
|
||||||
import styles from "./UploadField.module.css";
|
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";
|
const pagWasmURL = "/vendor/libpag.wasm";
|
||||||
let pagModulePromise;
|
let pagModulePromise;
|
||||||
|
|
||||||
@ -50,10 +51,12 @@ export function UploadField({
|
|||||||
const [downloading, setDownloading] = useState(false);
|
const [downloading, setDownloading] = useState(false);
|
||||||
const [videoPreviewOpen, setVideoPreviewOpen] = useState(false);
|
const [videoPreviewOpen, setVideoPreviewOpen] = useState(false);
|
||||||
const isImage = kind === "image";
|
const isImage = kind === "image";
|
||||||
|
const uploadMode = uploadKind || kind;
|
||||||
|
const useImageUpload = uploadMode === "image";
|
||||||
const source = localPreview?.url || value;
|
const source = localPreview?.url || value;
|
||||||
const assetKind = localPreview?.assetKind || getAssetKind(value, kind);
|
const assetKind = localPreview?.assetKind || getAssetKind(value, kind);
|
||||||
const displayName = localPreview?.name || getDisplayValue(value);
|
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 canPreviewVideo = Boolean(source && assetKind === "mp4");
|
||||||
const localPreviewURL = localPreview?.url;
|
const localPreviewURL = localPreview?.url;
|
||||||
const previewMp4Layout = Object.prototype.hasOwnProperty.call(localPreview?.selectionResult || {}, "mp4Layout")
|
const previewMp4Layout = Object.prototype.hasOwnProperty.call(localPreview?.selectionResult || {}, "mp4Layout")
|
||||||
@ -112,9 +115,7 @@ export function UploadField({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setLocalPreview((previous) => (previous ? { ...previous, selectionResult } : previous));
|
setLocalPreview((previous) => (previous ? { ...previous, selectionResult } : previous));
|
||||||
// 图片接口会校验固定的位图格式;资源素材不能被这层校验变成格式白名单,
|
const result = useImageUpload ? await uploadImage(file) : await uploadFile(file);
|
||||||
// 因此仅 JPEG/PNG/WebP 走图片接口,其余 PAG/SVGA/GIF/AVIF 或未知格式走通用文件接口。
|
|
||||||
const result = shouldUploadAsImage(file, kind, uploadKind) ? await uploadImage(file) : await uploadFile(file);
|
|
||||||
if (uploadGeneration !== uploadGenerationRef.current) {
|
if (uploadGeneration !== uploadGenerationRef.current) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -549,20 +550,6 @@ function getAssetKind(value, kind, contentType = "") {
|
|||||||
return kind === "image" ? "image" : "file";
|
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) {
|
function isRasterImageExtension(extension) {
|
||||||
return ["avif", "gif", "jpg", "jpeg", "png", "webp"].includes(extension);
|
return ["avif", "gif", "jpg", "jpeg", "png", "webp"].includes(extension);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
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 { downloadResponse } from "@/shared/api/download";
|
||||||
import { uploadFile, uploadImage } from "@/shared/api/upload";
|
import { uploadFile, uploadImage } from "@/shared/api/upload";
|
||||||
import { ToastProvider } from "@/shared/ui/ToastProvider.jsx";
|
import { ToastProvider } from "@/shared/ui/ToastProvider.jsx";
|
||||||
@ -14,10 +14,6 @@ vi.mock("@/shared/api/download", () => ({
|
|||||||
downloadResponse: vi.fn(async () => undefined),
|
downloadResponse: vi.fn(async () => undefined),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
vi.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("file upload field does not restrict picker type and uses generic upload endpoint", async () => {
|
test("file upload field does not restrict picker type and uses generic upload endpoint", async () => {
|
||||||
const onChange = vi.fn();
|
const onChange = vi.fn();
|
||||||
const onFileSelected = 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");
|
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 () => {
|
test("mp4 file upload field can open video preview dialog", async () => {
|
||||||
const source = "https://media.haiyihy.com/admin/files/gift.mp4?sign=1";
|
const source = "https://media.haiyihy.com/admin/files/gift.mp4?sign=1";
|
||||||
const onChange = vi.fn();
|
const onChange = vi.fn();
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user