送礼记录

This commit is contained in:
zhx 2026-07-14 20:23:03 +08:00
parent 59901a3cab
commit da6d2dfdb9
15 changed files with 811 additions and 1 deletions

View File

@ -3206,6 +3206,71 @@
"x-permissions": ["coin-adjustment:create"] "x-permissions": ["coin-adjustment:create"]
} }
}, },
"/admin/operations/gift-records": {
"get": {
"operationId": "listGiftRecords",
"responses": {
"200": {
"$ref": "#/components/responses/GiftRecordPageResponse"
}
},
"parameters": [
{
"$ref": "#/components/parameters/Page"
},
{
"$ref": "#/components/parameters/PageSize"
},
{
"name": "scene",
"in": "query",
"schema": {
"type": "string",
"enum": ["room", "direct"]
}
},
{
"name": "sender_display_user_id",
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "sender_user_id",
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "sender_username",
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "start_at_ms",
"in": "query",
"schema": {
"type": "integer",
"format": "int64"
}
},
{
"name": "end_at_ms",
"in": "query",
"schema": {
"type": "integer",
"format": "int64"
}
}
],
"x-permission": "gift-record:view",
"x-permissions": ["gift-record:view"]
}
},
"/admin/operations/coin-ledger": { "/admin/operations/coin-ledger": {
"get": { "get": {
"operationId": "listCoinLedger", "operationId": "listCoinLedger",
@ -7810,6 +7875,16 @@
} }
} }
}, },
"GiftRecordPageResponse": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiResponseGiftRecordPage"
}
}
}
},
"BDProfilePageResponse": { "BDProfilePageResponse": {
"description": "OK", "description": "OK",
"content": { "content": {
@ -9289,6 +9364,27 @@
} }
} }
}, },
"ApiPageGiftRecord": {
"type": "object",
"required": ["items", "page", "pageSize", "total"],
"properties": {
"items": {
"type": "array",
"items": {
"$ref": "#/components/schemas/GiftRecord"
}
},
"page": {
"type": "integer"
},
"pageSize": {
"type": "integer"
},
"total": {
"type": "integer"
}
}
},
"ApiPageBDProfile": { "ApiPageBDProfile": {
"type": "object", "type": "object",
"required": ["items", "page", "pageSize", "total"], "required": ["items", "page", "pageSize", "total"],
@ -9669,6 +9765,21 @@
} }
] ]
}, },
"ApiResponseGiftRecordPage": {
"allOf": [
{
"$ref": "#/components/schemas/Envelope"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/components/schemas/ApiPageGiftRecord"
}
}
}
]
},
"ApiResponseBDProfilePage": { "ApiResponseBDProfilePage": {
"allOf": [ "allOf": [
{ {
@ -10878,6 +10989,99 @@
} }
} }
}, },
"GiftRecordUser": {
"type": "object",
"required": ["userId"],
"properties": {
"avatar": {
"type": "string"
},
"defaultDisplayUserId": {
"type": "string"
},
"displayUserId": {
"type": "string"
},
"prettyDisplayUserId": {
"type": "string"
},
"prettyId": {
"type": "string"
},
"userId": {
"type": "string"
},
"username": {
"type": "string"
}
}
},
"GiftRecordGift": {
"type": "object",
"required": ["giftId", "name", "coverUrl"],
"properties": {
"coverUrl": {
"type": "string"
},
"giftId": {
"type": "string"
},
"name": {
"type": "string"
}
}
},
"GiftRecord": {
"type": "object",
"required": [
"transactionId",
"scene",
"sender",
"receiver",
"gift",
"giftCount",
"unitValue",
"giftValue",
"createdAtMs"
],
"properties": {
"createdAtMs": {
"type": "integer",
"format": "int64"
},
"gift": {
"$ref": "#/components/schemas/GiftRecordGift"
},
"giftCount": {
"type": "integer",
"format": "int32"
},
"giftValue": {
"type": "integer",
"format": "int64"
},
"receiver": {
"$ref": "#/components/schemas/GiftRecordUser"
},
"roomId": {
"type": "string"
},
"scene": {
"type": "string",
"enum": ["room", "direct"]
},
"sender": {
"$ref": "#/components/schemas/GiftRecordUser"
},
"transactionId": {
"type": "string"
},
"unitValue": {
"type": "integer",
"format": "int64"
}
}
},
"CoinLedgerUser": { "CoinLedgerUser": {
"type": "object", "type": "object",
"required": ["userId"], "required": ["userId"],

View File

@ -186,6 +186,7 @@ export const fallbackNavigation = [
id: "operations", id: "operations",
label: "运营管理", label: "运营管理",
children: [ children: [
routeNavItem("operation-gift-records", { icon: CardGiftcardOutlined }),
routeNavItem("operation-coin-ledger", { icon: ReceiptLongOutlined }), routeNavItem("operation-coin-ledger", { icon: ReceiptLongOutlined }),
routeNavItem("operation-coin-seller-ledger", { icon: ReceiptLongOutlined }), routeNavItem("operation-coin-seller-ledger", { icon: ReceiptLongOutlined }),
routeNavItem("operation-coin-adjustment", { icon: WalletOutlined }), routeNavItem("operation-coin-adjustment", { icon: WalletOutlined }),

View File

@ -57,6 +57,7 @@ export const PERMISSIONS = {
financeOrderCoinSellerRechargeGrant: "finance-order:coin-seller-recharge:grant", financeOrderCoinSellerRechargeGrant: "finance-order:coin-seller-recharge:grant",
coinLedgerView: "coin-ledger:view", coinLedgerView: "coin-ledger:view",
coinSellerLedgerView: "coin-seller-ledger:view", coinSellerLedgerView: "coin-seller-ledger:view",
giftRecordView: "gift-record:view",
coinAdjustmentView: "coin-adjustment:view", coinAdjustmentView: "coin-adjustment:view",
coinAdjustmentCreate: "coin-adjustment:create", coinAdjustmentCreate: "coin-adjustment:create",
reportView: "report:view", reportView: "report:view",
@ -250,6 +251,7 @@ export const MENU_CODES = {
operations: "operations", operations: "operations",
operationCoinLedger: "operation-coin-ledger", operationCoinLedger: "operation-coin-ledger",
operationCoinSellerLedger: "operation-coin-seller-ledger", operationCoinSellerLedger: "operation-coin-seller-ledger",
operationGiftRecords: "operation-gift-records",
operationCoinAdjustment: "operation-coin-adjustment", operationCoinAdjustment: "operation-coin-adjustment",
operationReports: "operation-reports", operationReports: "operation-reports",
operationGiftDiamond: "operation-gift-diamond", operationGiftDiamond: "operation-gift-diamond",

View File

@ -8,7 +8,7 @@ export interface GamePlatformDto {
platformName: string; platformName: string;
status: string; status: string;
apiBaseUrl: string; apiBaseUrl: string;
// 服务端按 adapterType 选择真实厂商协议yomi_v4/leadercc_v1/zeeone_v1/baishun_v1/vivagames_v1/reyou_v1 // 服务端按 adapterType 选择真实厂商协议AMG 使用 amg_v1RSA 私钥仍通过 callbackSecret 单独提交
adapterType: string; adapterType: string;
// callbackSecret 由后台列表返回,用于配置页直接核对和轮换厂商 key。 // callbackSecret 由后台列表返回,用于配置页直接核对和轮换厂商 key。
callbackSecret?: string; callbackSecret?: string;

View File

@ -65,6 +65,7 @@ const writableBridgeAdapterTypes = new Set([
"vivagames_v1", "vivagames_v1",
"reyou_v1", "reyou_v1",
"zgame_v1", "zgame_v1",
"amg_v1",
]); ]);
const emptyCatalog = { items: [], pageSize: 50 }; const emptyCatalog = { items: [], pageSize: 50 };

View File

@ -54,6 +54,7 @@ const adapterTypeOptions = [
["vivagames_v1", "VIVAGAMES V1"], ["vivagames_v1", "VIVAGAMES V1"],
["reyou_v1", "热游 Reyou V1"], ["reyou_v1", "热游 Reyou V1"],
["zgame_v1", "ZGame V1"], ["zgame_v1", "ZGame V1"],
["amg_v1", "AMG V1"],
]; ];
const baseColumns = [ const baseColumns = [

View File

@ -9,6 +9,7 @@ import {
listCoinLedger, listCoinLedger,
listCoinAdjustments, listCoinAdjustments,
listCoinSellerLedger, listCoinSellerLedger,
listGiftRecords,
listWheelDraws, listWheelDraws,
lookupCoinAdjustmentTarget, lookupCoinAdjustmentTarget,
updateGiftDiamondRatios, updateGiftDiamondRatios,
@ -110,6 +111,36 @@ test("coin ledger API sends type filter", async () => {
expect(listInit?.method).toBe("GET"); expect(listInit?.method).toBe("GET");
}); });
test("gift record API uses generated path with sender, scene and UTC range filters", async () => {
vi.stubGlobal(
"fetch",
vi.fn(
async () => new Response(JSON.stringify({ code: 0, data: { items: [], page: 1, pageSize: 30, total: 0 } })),
),
);
await listGiftRecords({
end_at_ms: 2000,
page: 1,
page_size: 30,
scene: "room",
sender_display_user_id: "888888",
sender_user_id: "1001",
sender_username: "送礼人",
start_at_ms: 1000,
});
const [listUrl, listInit] = vi.mocked(fetch).mock.calls[0];
expect(String(listUrl)).toContain("/api/v1/admin/operations/gift-records?");
expect(String(listUrl)).toContain("scene=room");
expect(String(listUrl)).toContain("sender_display_user_id=888888");
expect(String(listUrl)).toContain("sender_user_id=1001");
expect(String(listUrl)).toContain("sender_username=%E9%80%81%E7%A4%BC%E4%BA%BA");
expect(String(listUrl)).toContain("start_at_ms=1000");
expect(String(listUrl)).toContain("end_at_ms=2000");
expect(listInit?.method).toBe("GET");
});
test("coin seller ledger API uses generated admin path and filters", async () => { test("coin seller ledger API uses generated admin path and filters", async () => {
vi.stubGlobal( vi.stubGlobal(
"fetch", "fetch",

View File

@ -22,6 +22,33 @@ export interface GiftDiamondRatioItem {
updatedAtMs?: number; updatedAtMs?: number;
} }
export interface GiftRecordUserDto {
userId: string;
displayUserId?: string;
defaultDisplayUserId?: string;
prettyDisplayUserId?: string;
prettyId?: string;
username?: string;
avatar?: string;
}
export interface GiftRecordDto {
transactionId: string;
scene: "room" | "direct";
roomId?: string;
sender: GiftRecordUserDto;
receiver: GiftRecordUserDto;
gift: {
giftId: string;
name: string;
coverUrl?: string;
};
giftCount: number;
unitValue: number;
giftValue: number;
createdAtMs: number;
}
export interface GiftDiamondRatioResponse { export interface GiftDiamondRatioResponse {
regionId: number; regionId: number;
items: GiftDiamondRatioItem[]; items: GiftDiamondRatioItem[];
@ -132,6 +159,14 @@ export function listCoinLedger(query: PageQuery = {}): Promise<ApiPage<CoinLedge
}); });
} }
export function listGiftRecords(query: PageQuery = {}): Promise<ApiPage<GiftRecordDto>> {
const endpoint = API_ENDPOINTS.listGiftRecords;
return apiRequest<ApiPage<GiftRecordDto>>(apiEndpointPath(API_OPERATIONS.listGiftRecords), {
method: endpoint.method,
query,
});
}
export function listCoinSellerLedger(query: PageQuery = {}): Promise<ApiPage<CoinSellerLedgerDto>> { export function listCoinSellerLedger(query: PageQuery = {}): Promise<ApiPage<CoinSellerLedgerDto>> {
const endpoint = API_ENDPOINTS.listCoinSellerLedger; const endpoint = API_ENDPOINTS.listCoinSellerLedger;
return apiRequest<ApiPage<CoinSellerLedgerDto>>(apiEndpointPath(API_OPERATIONS.listCoinSellerLedger), { return apiRequest<ApiPage<CoinSellerLedgerDto>>(apiEndpointPath(API_OPERATIONS.listCoinSellerLedger), {

View File

@ -0,0 +1,220 @@
import CardGiftcardOutlined from "@mui/icons-material/CardGiftcardOutlined";
import { useMemo, useState } from "react";
import { listGiftRecords } from "@/features/operations/api";
import styles from "@/features/operations/operations.module.css";
import { usePaginatedQuery } from "@/shared/hooks/usePaginatedQuery.js";
import { AdminFilterResetButton, AdminListBody, AdminListToolbar } from "@/shared/ui/AdminListLayout.jsx";
import { AdminUserIdentity } from "@/shared/ui/AdminUserIdentity.jsx";
import { DataState } from "@/shared/ui/DataState.jsx";
import { DataTable } from "@/shared/ui/DataTable.jsx";
import { TimeRangeFilter } from "@/shared/ui/TimeRangeFilter.jsx";
import { TimeText } from "@/shared/ui/TimeText.jsx";
import { createOptionsColumnFilter, createUserIdentityColumnFilter } from "@/shared/ui/tableFilters.js";
import {
emptyUserIdentityFilter,
isUserIdentityFilterEmpty,
userIdentityFilterParams,
} from "@/shared/ui/userIdentityFilter.js";
const pageSize = 30;
const sceneOptions = [
{ label: "房间送礼", value: "room" },
{ label: "私聊送礼", value: "direct" },
];
export function GiftRecordTable() {
const [page, setPage] = useState(1);
const [scene, setScene] = useState("");
const [senderFilter, setSenderFilter] = useState(emptyUserIdentityFilter);
const [timeRange, setTimeRange] = useState({ endMs: "", startMs: "" });
const filters = useMemo(
() => ({
end_at_ms: timeRange.endMs || "",
scene,
...userIdentityFilterParams(senderFilter, {
displayUserId: "sender_display_user_id",
userId: "sender_user_id",
username: "sender_username",
}),
start_at_ms: timeRange.startMs || "",
}),
[scene, senderFilter, timeRange.endMs, timeRange.startMs],
);
const query = usePaginatedQuery({
errorMessage: "获取送礼记录失败",
fetcher: listGiftRecords,
filters,
page,
pageSize,
queryKey: ["gift-records", filters, page],
});
const data = query.data || { items: [], page, pageSize, total: 0 };
const items = data.items || [];
const total = data.total || 0;
const hasActiveFilters = Boolean(
!isUserIdentityFilterEmpty(senderFilter) || scene || timeRange.startMs || timeRange.endMs,
);
const changeScene = (value) => {
setScene(value);
setPage(1);
};
const changeSenderFilter = (value) => {
setSenderFilter(value);
setPage(1);
};
const changeTimeRange = (value) => {
setTimeRange(value);
setPage(1);
};
const resetFilters = () => {
setScene("");
setSenderFilter(emptyUserIdentityFilter);
setTimeRange({ endMs: "", startMs: "" });
setPage(1);
};
const columns = useMemo(
() => [
{
key: "index",
label: "序号",
render: (_entry, index) => <span className={styles.giftRecordIndex}>{index + 1}</span>,
resizable: false,
width: "72px",
},
{
filter: createUserIdentityColumnFilter({
value: senderFilter,
onChange: changeSenderFilter,
}),
key: "sender",
label: "送礼人",
render: (entry) => <AdminUserIdentity openInAppUserDetail user={entry.sender} />,
width: "minmax(210px, 1fr)",
},
{
key: "receiver",
label: "收礼人",
render: (entry) => <AdminUserIdentity openInAppUserDetail user={entry.receiver} />,
width: "minmax(210px, 1fr)",
},
{
key: "gift",
label: "礼物",
render: (entry) => <GiftCell gift={entry.gift} />,
width: "minmax(220px, 1fr)",
},
{
key: "giftValue",
label: "数量与价值",
render: (entry) => <GiftValueCell entry={entry} />,
width: "minmax(190px, 0.9fr)",
},
{
filter: createOptionsColumnFilter({
emptyLabel: "全部场景",
options: sceneOptions,
placeholder: "送礼场景",
value: scene,
onChange: changeScene,
}),
key: "scene",
label: "场景",
render: (entry) => <SceneCell entry={entry} />,
width: "minmax(132px, 0.7fr)",
},
{
key: "createdAtMs",
label: "送礼时间",
render: (entry) => <TimeText value={entry.createdAtMs} />,
width: "minmax(170px, 0.8fr)",
},
],
[scene, senderFilter],
);
return (
<div className={styles.ledgerTableShell}>
<AdminListToolbar
filters={
<>
<TimeRangeFilter value={timeRange} onChange={changeTimeRange} />
<AdminFilterResetButton disabled={!hasActiveFilters} onClick={resetFilters} />
</>
}
/>
<DataState error={query.error} loading={query.loading} onRetry={query.reload}>
<AdminListBody>
<DataTable
columns={columns}
emptyLabel="当前无送礼记录"
items={items}
minWidth="1230px"
pagination={
total > 0
? {
page,
pageSize: data.pageSize || pageSize,
total,
onPageChange: setPage,
}
: undefined
}
rowKey={(entry) => entry.transactionId}
/>
</AdminListBody>
</DataState>
</div>
);
}
function GiftCell({ gift = {} }) {
return (
<div className={styles.giftRecordGift}>
<span className={styles.giftRecordCover}>
{gift.coverUrl ? <img src={gift.coverUrl} alt="" /> : <CardGiftcardOutlined fontSize="small" />}
</span>
<span className={styles.giftRecordGiftText}>
<span className={styles.giftRecordGiftName}>{gift.name || "-"}</span>
{gift.giftId ? <span className={styles.giftRecordGiftId}>{gift.giftId}</span> : null}
</span>
</div>
);
}
function GiftValueCell({ entry }) {
return (
<div className={styles.giftRecordMetrics}>
<span className={styles.giftRecordMetric}>
<span>数量</span>
<strong>× {formatNumber(entry.giftCount)}</strong>
</span>
<span className={styles.giftRecordMetric}>
<span>单价</span>
<strong>{formatNumber(entry.unitValue)}</strong>
</span>
<span className={`${styles.giftRecordMetric} ${styles.giftRecordTotal}`}>
<span>总价值</span>
<strong>{formatNumber(entry.giftValue)}</strong>
</span>
</div>
);
}
function SceneCell({ entry }) {
const direct = entry.scene === "direct";
return (
<span className={styles.giftRecordScene}>
<span className={`${styles.giftRecordSceneBadge} ${direct ? styles.giftRecordSceneDirect : styles.giftRecordSceneRoom}`}>
{direct ? "私聊" : "房间"}
</span>
{!direct && entry.roomId ? <span className={styles.giftRecordRoomId}>{entry.roomId}</span> : null}
</span>
);
}
function formatNumber(value) {
return Number(value || 0).toLocaleString("zh-CN");
}

View File

@ -0,0 +1,69 @@
import { fireEvent, render, screen } from "@testing-library/react";
import { afterEach, expect, test, vi } from "vitest";
import { GiftRecordTable } from "@/features/operations/components/GiftRecordTable.jsx";
import { usePaginatedQuery } from "@/shared/hooks/usePaginatedQuery.js";
vi.mock("@/shared/hooks/usePaginatedQuery.js", () => ({
usePaginatedQuery: vi.fn(),
}));
afterEach(() => {
vi.clearAllMocks();
});
test("gift record table uses public user identities and keeps gift facts compact", () => {
vi.mocked(usePaginatedQuery).mockReturnValue({
data: {
items: [
{
createdAtMs: 1770000000000,
gift: { coverUrl: "https://cdn.example/rose.webp", giftId: "rose", name: "玫瑰" },
giftCount: 3,
giftValue: 300,
receiver: { displayUserId: "202", userId: "2002", username: "收礼人" },
roomId: "room-9",
scene: "room",
sender: {
defaultDisplayUserId: "101",
displayUserId: "888888",
prettyDisplayUserId: "888888",
prettyId: "pretty-1",
userId: "1001",
username: "送礼人",
},
transactionId: "wallet-gift-1",
unitValue: 100,
},
],
page: 1,
pageSize: 30,
total: 1,
},
error: null,
loading: false,
reload: vi.fn(),
});
render(<GiftRecordTable />);
expect(screen.getByText("1")).toBeInTheDocument();
expect(screen.getAllByText("送礼人").length).toBeGreaterThanOrEqual(2);
expect(screen.getAllByText("收礼人").length).toBeGreaterThanOrEqual(2);
expect(screen.getByText("玫瑰")).toBeInTheDocument();
expect(screen.getByText("× 3")).toBeInTheDocument();
expect(screen.getByText("300")).toBeInTheDocument();
expect(screen.getByText("房间")).toBeInTheDocument();
expect(screen.getByText("room-9")).toBeInTheDocument();
expect(screen.getByText("已加载 1 条 · 共 1 条")).toBeInTheDocument();
fireEvent.click(screen.getByRole("button", { name: "送礼人" }));
fireEvent.change(screen.getByPlaceholderText("输入短 ID"), { target: { value: "888888" } });
fireEvent.click(screen.getByRole("button", { name: "搜索" }));
expect(usePaginatedQuery).toHaveBeenLastCalledWith(
expect.objectContaining({
filters: expect.objectContaining({ sender_display_user_id: "888888" }),
page: 1,
}),
);
});

View File

@ -36,6 +36,140 @@
overflow: auto; overflow: auto;
} }
.giftRecordIndex {
color: var(--text-tertiary);
font-variant-numeric: tabular-nums;
font-weight: 750;
}
.giftRecordGift {
display: flex;
min-width: 0;
align-items: center;
gap: var(--space-3);
}
.giftRecordCover {
display: inline-flex;
width: 48px;
height: 48px;
flex: 0 0 auto;
align-items: center;
justify-content: center;
overflow: hidden;
border: 1px solid var(--border-soft);
border-radius: 14px;
background: linear-gradient(145deg, var(--bg-card-strong), var(--primary-hover));
color: var(--primary);
}
.giftRecordCover img {
width: 100%;
height: 100%;
object-fit: cover;
}
.giftRecordGiftText,
.giftRecordScene {
display: grid;
min-width: 0;
gap: 4px;
}
.giftRecordGiftName {
overflow: hidden;
color: var(--text-primary);
font-weight: 780;
text-overflow: ellipsis;
white-space: nowrap;
}
.giftRecordGiftId,
.giftRecordRoomId {
overflow: hidden;
color: var(--text-tertiary);
font-size: 12px;
text-overflow: ellipsis;
white-space: nowrap;
}
.giftRecordMetrics {
display: grid;
min-width: 0;
grid-template-columns: repeat(3, minmax(0, auto));
align-items: center;
gap: var(--space-3);
}
.giftRecordMetric {
display: grid;
min-width: 0;
gap: 3px;
}
.giftRecordMetric span {
color: var(--text-tertiary);
font-size: 11px;
white-space: nowrap;
}
.giftRecordMetric strong {
color: var(--text-primary);
font-size: var(--admin-font-size);
font-variant-numeric: tabular-nums;
font-weight: 760;
white-space: nowrap;
}
.giftRecordTotal {
padding-left: var(--space-3);
border-left: 1px solid var(--border-soft);
}
.giftRecordTotal strong {
color: var(--primary);
}
.giftRecordSceneBadge {
display: inline-flex;
width: max-content;
min-height: 24px;
align-items: center;
padding: 0 9px;
border: 1px solid transparent;
border-radius: 999px;
font-size: 12px;
font-weight: 760;
}
.giftRecordSceneRoom {
border-color: var(--primary-border);
background: var(--primary-hover);
color: var(--primary);
}
.giftRecordSceneDirect {
border-color: rgba(124, 58, 237, 0.2);
background: rgba(124, 58, 237, 0.08);
color: #7c3aed;
}
@media (prefers-reduced-motion: no-preference) {
.giftRecordCover,
.giftRecordSceneBadge {
transition:
border-color 160ms ease,
box-shadow 160ms ease,
transform 160ms ease;
}
.giftRecordGift:hover .giftRecordCover {
border-color: var(--primary-border-strong);
box-shadow: 0 8px 18px rgba(37, 99, 235, 0.1);
transform: translateY(-1px);
}
}
.adjustmentAmount { .adjustmentAmount {
font-size: calc(var(--admin-font-size) + 4px); font-size: calc(var(--admin-font-size) + 4px);
} }

View File

@ -0,0 +1,10 @@
import { GiftRecordTable } from "@/features/operations/components/GiftRecordTable.jsx";
import { AdminListPage } from "@/shared/ui/AdminListLayout.jsx";
export function GiftRecordPage() {
return (
<AdminListPage>
<GiftRecordTable />
</AdminListPage>
);
}

View File

@ -1,6 +1,14 @@
import { MENU_CODES, PERMISSIONS } from "@/app/permissions"; import { MENU_CODES, PERMISSIONS } from "@/app/permissions";
export const operationsRoutes = [ export const operationsRoutes = [
{
label: "送礼记录",
loader: () => import("./pages/GiftRecordPage.jsx").then((module) => module.GiftRecordPage),
menuCode: MENU_CODES.operationGiftRecords,
pageKey: "operation-gift-records",
path: "/operations/gift-records",
permission: PERMISSIONS.giftRecordView,
},
{ {
label: "金币流水", label: "金币流水",
loader: () => import("./pages/CoinLedgerPage.jsx").then((module) => module.CoinLedgerPage), loader: () => import("./pages/CoinLedgerPage.jsx").then((module) => module.CoinLedgerPage),

View File

@ -203,6 +203,7 @@ export const API_OPERATIONS = {
listFinanceWithdrawalApplications: "listFinanceWithdrawalApplications", listFinanceWithdrawalApplications: "listFinanceWithdrawalApplications",
listFirstRechargeRewardClaims: "listFirstRechargeRewardClaims", listFirstRechargeRewardClaims: "listFirstRechargeRewardClaims",
listGameWhitelistUsers: "listGameWhitelistUsers", listGameWhitelistUsers: "listGameWhitelistUsers",
listGiftRecords: "listGiftRecords",
listGifts: "listGifts", listGifts: "listGifts",
listGiftTypes: "listGiftTypes", listGiftTypes: "listGiftTypes",
listH5Links: "listH5Links", listH5Links: "listH5Links",
@ -1710,6 +1711,13 @@ export const API_ENDPOINTS: Record<ApiOperationId, ApiEndpoint> = {
permission: "game:view", permission: "game:view",
permissions: ["game:view"] permissions: ["game:view"]
}, },
listGiftRecords: {
method: "GET",
operationId: API_OPERATIONS.listGiftRecords,
path: "/v1/admin/operations/gift-records",
permission: "gift-record:view",
permissions: ["gift-record:view"]
},
listGifts: { listGifts: {
method: "GET", method: "GET",
operationId: API_OPERATIONS.listGifts, operationId: API_OPERATIONS.listGifts,

View File

@ -2060,6 +2060,22 @@ export interface paths {
patch?: never; patch?: never;
trace?: never; trace?: never;
}; };
"/admin/operations/gift-records": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get: operations["listGiftRecords"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/admin/operations/coin-ledger": { "/admin/operations/coin-ledger": {
parameters: { parameters: {
query?: never; query?: never;
@ -4887,6 +4903,12 @@ export interface components {
pageSize: number; pageSize: number;
total: number; total: number;
}; };
ApiPageGiftRecord: {
items: components["schemas"]["GiftRecord"][];
page: number;
pageSize: number;
total: number;
};
ApiPageBDProfile: { ApiPageBDProfile: {
items: components["schemas"]["BDProfile"][]; items: components["schemas"]["BDProfile"][];
page: number; page: number;
@ -5043,6 +5065,9 @@ export interface components {
ApiResponseCoinSellerLedgerPage: components["schemas"]["Envelope"] & { ApiResponseCoinSellerLedgerPage: components["schemas"]["Envelope"] & {
data?: components["schemas"]["ApiPageCoinSellerLedger"]; data?: components["schemas"]["ApiPageCoinSellerLedger"];
}; };
ApiResponseGiftRecordPage: components["schemas"]["Envelope"] & {
data?: components["schemas"]["ApiPageGiftRecord"];
};
ApiResponseBDProfilePage: components["schemas"]["Envelope"] & { ApiResponseBDProfilePage: components["schemas"]["Envelope"] & {
data?: components["schemas"]["ApiPageBDProfile"]; data?: components["schemas"]["ApiPageBDProfile"];
}; };
@ -5403,6 +5428,37 @@ export interface components {
child?: components["schemas"]["CoinSellerSubApplicationReviewChild"]; child?: components["schemas"]["CoinSellerSubApplicationReviewChild"];
relation?: components["schemas"]["CoinSellerSubRelation"]; relation?: components["schemas"]["CoinSellerSubRelation"];
}; };
GiftRecordUser: {
avatar?: string;
defaultDisplayUserId?: string;
displayUserId?: string;
prettyDisplayUserId?: string;
prettyId?: string;
userId: string;
username?: string;
};
GiftRecordGift: {
coverUrl: string;
giftId: string;
name: string;
};
GiftRecord: {
/** Format: int64 */
createdAtMs: number;
gift: components["schemas"]["GiftRecordGift"];
/** Format: int32 */
giftCount: number;
/** Format: int64 */
giftValue: number;
receiver: components["schemas"]["GiftRecordUser"];
roomId?: string;
/** @enum {string} */
scene: "room" | "direct";
sender: components["schemas"]["GiftRecordUser"];
transactionId: string;
/** Format: int64 */
unitValue: number;
};
CoinLedgerUser: { CoinLedgerUser: {
avatar?: string; avatar?: string;
displayUserId?: string; displayUserId?: string;
@ -6147,6 +6203,15 @@ export interface components {
}; };
}; };
/** @description OK */ /** @description OK */
GiftRecordPageResponse: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiResponseGiftRecordPage"];
};
};
/** @description OK */
BDProfilePageResponse: { BDProfilePageResponse: {
headers: { headers: {
[name: string]: unknown; [name: string]: unknown;
@ -9193,6 +9258,27 @@ export interface operations {
200: components["responses"]["EmptyResponse"]; 200: components["responses"]["EmptyResponse"];
}; };
}; };
listGiftRecords: {
parameters: {
query?: {
page?: components["parameters"]["Page"];
page_size?: components["parameters"]["PageSize"];
scene?: "room" | "direct";
sender_display_user_id?: string;
sender_user_id?: string;
sender_username?: string;
start_at_ms?: number;
end_at_ms?: number;
};
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
200: components["responses"]["GiftRecordPageResponse"];
};
};
listCoinLedger: { listCoinLedger: {
parameters: { parameters: {
query?: { query?: {