数据改动
This commit is contained in:
parent
50da0a569b
commit
c7ca4f23ec
@ -2836,9 +2836,165 @@
|
||||
"operationId": "listCoinSellerLedger",
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "#/components/responses/EmptyResponse"
|
||||
"$ref": "#/components/responses/CoinSellerLedgerPageResponse"
|
||||
}
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "#/components/parameters/Page"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/parameters/PageSize"
|
||||
},
|
||||
{
|
||||
"name": "seller_user_id",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "seller_keyword",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "seller_filter_user_id",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "seller_display_user_id",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "seller_username",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ledger_type",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"enum": ["admin_stock_credit", "seller_transfer", "sub_seller_transfer", "salary_transfer_received"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "start_at_ms",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "end_at_ms",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
}
|
||||
}
|
||||
],
|
||||
"x-permission": "coin-seller-ledger:view",
|
||||
"x-permissions": ["coin-seller-ledger:view"]
|
||||
}
|
||||
},
|
||||
"/admin/operations/coin-seller-ledger/export": {
|
||||
"get": {
|
||||
"operationId": "exportCoinSellerLedger",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "CSV",
|
||||
"content": {
|
||||
"text/csv": {
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "binary"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "#/components/parameters/Page"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/parameters/PageSize"
|
||||
},
|
||||
{
|
||||
"name": "seller_user_id",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "seller_keyword",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "seller_filter_user_id",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "seller_display_user_id",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "seller_username",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ledger_type",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"enum": ["admin_stock_credit", "seller_transfer", "sub_seller_transfer", "salary_transfer_received"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "start_at_ms",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "end_at_ms",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
}
|
||||
}
|
||||
],
|
||||
"x-permission": "coin-seller-ledger:view",
|
||||
"x-permissions": ["coin-seller-ledger:view"]
|
||||
}
|
||||
@ -3552,6 +3708,50 @@
|
||||
"x-permissions": ["overview:view"]
|
||||
}
|
||||
},
|
||||
"/admin/databi/social/requirements": {
|
||||
"get": {
|
||||
"operationId": "getSocialBiRequirements",
|
||||
"x-permission": "overview:view",
|
||||
"parameters": [
|
||||
{ "in": "query", "name": "stat_tz", "schema": { "type": "string" } },
|
||||
{ "in": "query", "name": "start_ms", "schema": { "type": "integer", "format": "int64" } },
|
||||
{ "in": "query", "name": "end_ms", "schema": { "type": "integer", "format": "int64" } },
|
||||
{ "in": "query", "name": "app_codes", "schema": { "type": "string" } },
|
||||
{ "in": "query", "name": "region_id", "schema": { "type": "integer", "format": "int64" } },
|
||||
{ "in": "query", "name": "region_ids", "description": "逗号分隔的大区 ID;用于 Social BI 顶栏多选区域。", "schema": { "type": "string" } },
|
||||
{
|
||||
"in": "query",
|
||||
"name": "section",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"enum": ["new_users", "revenue", "retention", "active", "gift", "game"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "user_role",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"enum": ["all", "host", "user"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "payer_type",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"enum": ["all", "paid", "unpaid"]
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "#/components/responses/SocialBiRequirementsResponse"
|
||||
}
|
||||
},
|
||||
"x-permissions": ["overview:view"]
|
||||
}
|
||||
},
|
||||
"/admin/databi/social/funnel": {
|
||||
"get": {
|
||||
"operationId": "getSocialBiFunnel",
|
||||
@ -6861,6 +7061,16 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"CoinSellerLedgerPageResponse": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ApiResponseCoinSellerLedgerPage"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"BDProfilePageResponse": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
@ -7171,6 +7381,16 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"SocialBiRequirementsResponse": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ApiResponseSocialBiRequirements"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"TeamListResponse": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
@ -7297,6 +7517,27 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"ApiPageCoinSellerLedger": {
|
||||
"type": "object",
|
||||
"required": ["items", "page", "pageSize", "total"],
|
||||
"properties": {
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/CoinSellerLedger"
|
||||
}
|
||||
},
|
||||
"page": {
|
||||
"type": "integer"
|
||||
},
|
||||
"pageSize": {
|
||||
"type": "integer"
|
||||
},
|
||||
"total": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ApiPageBDProfile": {
|
||||
"type": "object",
|
||||
"required": ["items", "page", "pageSize", "total"],
|
||||
@ -7653,6 +7894,21 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"ApiResponseCoinSellerLedgerPage": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/Envelope"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/components/schemas/ApiPageCoinSellerLedger"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"ApiResponseBDProfilePage": {
|
||||
"allOf": [
|
||||
{
|
||||
@ -7777,6 +8033,159 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"ApiResponseSocialBiRequirements": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/Envelope"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/components/schemas/SocialBiRequirementsResult"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"SocialBiRequirementsResult": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"apps": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/SocialBiRequirementsApp"
|
||||
}
|
||||
},
|
||||
"access": {
|
||||
"$ref": "#/components/schemas/SocialBiRequirementsAccess"
|
||||
}
|
||||
},
|
||||
"required": ["apps"]
|
||||
},
|
||||
"SocialBiRequirementsApp": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"app_code": { "type": "string" },
|
||||
"app_name": { "type": "string" },
|
||||
"kind": { "type": "string" },
|
||||
"restricted": { "type": "boolean" },
|
||||
"allowed_region_ids": {
|
||||
"type": "array",
|
||||
"items": { "type": "integer", "format": "int64" }
|
||||
},
|
||||
"sections": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/SocialBiRequirementsSection"
|
||||
}
|
||||
},
|
||||
"updated_at_ms": { "type": "integer", "format": "int64" },
|
||||
"error": { "type": "string" }
|
||||
},
|
||||
"required": ["app_code", "app_name", "kind", "sections"]
|
||||
},
|
||||
"SocialBiRequirementsSection": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"key": {
|
||||
"type": "string",
|
||||
"enum": ["new_users", "revenue", "retention", "active", "gift", "game"]
|
||||
},
|
||||
"label": { "type": "string" },
|
||||
"filters": {
|
||||
"$ref": "#/components/schemas/SocialBiRequirementsFilters"
|
||||
},
|
||||
"columns": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/SocialBiRequirementsColumn"
|
||||
}
|
||||
},
|
||||
"metric_definitions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/SocialBiRequirementsMetricDefinition"
|
||||
}
|
||||
},
|
||||
"total": {
|
||||
"$ref": "#/components/schemas/SocialBiRequirementsRow"
|
||||
},
|
||||
"daily_series": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/SocialBiRequirementsRow"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["key", "columns", "daily_series"]
|
||||
},
|
||||
"SocialBiRequirementsFilters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"user_role": {
|
||||
"type": "string",
|
||||
"enum": ["all", "host", "user"]
|
||||
},
|
||||
"payer_type": {
|
||||
"type": "string",
|
||||
"enum": ["all", "paid", "unpaid"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"SocialBiRequirementsColumn": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"key": { "type": "string" },
|
||||
"label": { "type": "string" },
|
||||
"type": {
|
||||
"type": "string",
|
||||
"description": "count、ratio、money_minor、coin、duration_ms 或 number。"
|
||||
},
|
||||
"tooltip": { "type": "string" }
|
||||
},
|
||||
"required": ["key", "label", "type"]
|
||||
},
|
||||
"SocialBiRequirementsMetricDefinition": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"metric": { "type": "string" },
|
||||
"definition": { "type": "string" }
|
||||
},
|
||||
"required": ["metric", "definition"]
|
||||
},
|
||||
"SocialBiRequirementsRow": {
|
||||
"type": "object",
|
||||
"description": "日明细或汇总行;指标以 columns.key 对应的动态字段返回,兼容历史 metrics 对象。",
|
||||
"properties": {
|
||||
"stat_day": { "type": "string" },
|
||||
"label": { "type": "string" },
|
||||
"metrics": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"additionalProperties": true
|
||||
},
|
||||
"SocialBiRequirementsAccess": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"all": { "type": "boolean" },
|
||||
"scopes": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/SocialBiRequirementsScope"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"SocialBiRequirementsScope": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"app_code": { "type": "string" },
|
||||
"region_id": { "type": "integer", "format": "int64" }
|
||||
}
|
||||
},
|
||||
"ApiResponseFinanceApplication": {
|
||||
"allOf": [
|
||||
{
|
||||
@ -8385,6 +8794,124 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"CoinLedgerUser": {
|
||||
"type": "object",
|
||||
"required": ["userId"],
|
||||
"properties": {
|
||||
"avatar": {
|
||||
"type": "string"
|
||||
},
|
||||
"displayUserId": {
|
||||
"type": "string"
|
||||
},
|
||||
"userId": {
|
||||
"type": "string"
|
||||
},
|
||||
"username": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"CoinSellerLedger": {
|
||||
"type": "object",
|
||||
"required": ["entryId", "transactionId", "ledgerType", "bizType", "seller", "receiver", "amount", "direction", "availableDelta", "sellerBalanceAfter", "createdAtMs"],
|
||||
"properties": {
|
||||
"amount": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"availableDelta": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"bizType": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"coin_seller_stock_purchase",
|
||||
"coin_seller_stock_deduction",
|
||||
"coin_seller_recharge",
|
||||
"coin_seller_coin_compensation",
|
||||
"coin_seller_transfer",
|
||||
"coin_seller_sub_transfer",
|
||||
"salary_transfer_to_coin_seller",
|
||||
"manual_credit"
|
||||
]
|
||||
},
|
||||
"commandId": {
|
||||
"type": "string"
|
||||
},
|
||||
"counterpartyUserId": {
|
||||
"type": "string"
|
||||
},
|
||||
"createdAtMs": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"direction": {
|
||||
"type": "string",
|
||||
"enum": ["income", "expense"]
|
||||
},
|
||||
"entryId": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"ledgerType": {
|
||||
"type": "string",
|
||||
"enum": ["admin_stock_credit", "seller_transfer", "sub_seller_transfer", "salary_transfer_received"]
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
},
|
||||
"operator": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"adminId": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"username": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"operatorUserId": {
|
||||
"type": "string"
|
||||
},
|
||||
"paidAmountMicro": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"paidCurrencyCode": {
|
||||
"type": "string"
|
||||
},
|
||||
"reason": {
|
||||
"type": "string"
|
||||
},
|
||||
"receiver": {
|
||||
"$ref": "#/components/schemas/CoinLedgerUser"
|
||||
},
|
||||
"seller": {
|
||||
"$ref": "#/components/schemas/CoinLedgerUser"
|
||||
},
|
||||
"sellerBalanceAfter": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"stockType": {
|
||||
"type": "string"
|
||||
},
|
||||
"transactionId": {
|
||||
"type": "string"
|
||||
},
|
||||
"transferUsdMinor": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
}
|
||||
}
|
||||
},
|
||||
"CreateCoinSellerPayload": {
|
||||
"type": "object",
|
||||
"required": ["commandId", "targetUserId"],
|
||||
|
||||
@ -8,6 +8,7 @@ import {
|
||||
fetchSocialBiKpi,
|
||||
fetchSocialBiMaster,
|
||||
fetchSocialBiOverview,
|
||||
fetchSocialBiRequirements,
|
||||
fetchStatisticsOverview
|
||||
} from "./api.js";
|
||||
|
||||
@ -18,6 +19,7 @@ vi.mock("./api.js", () => ({
|
||||
fetchSocialBiKpi: vi.fn(),
|
||||
fetchSocialBiMaster: vi.fn(),
|
||||
fetchSocialBiOverview: vi.fn(),
|
||||
fetchSocialBiRequirements: vi.fn(),
|
||||
fetchSelfGameStatisticsOverview: vi.fn(),
|
||||
fetchStatisticsOverview: vi.fn(),
|
||||
getCurrentAppCode: vi.fn(() => "lalu"),
|
||||
@ -58,12 +60,14 @@ beforeEach(() => {
|
||||
fetchSocialBiOverview.mockResolvedValue({ access: { all: true, scopes: [] }, apps: [] });
|
||||
fetchSocialBiFunnel.mockResolvedValue({ access: { all: true, scopes: [] }, apps: [] });
|
||||
fetchSocialBiKpi.mockResolvedValue({ items: [], period_month: "2026-06", summary: {} });
|
||||
fetchSocialBiRequirements.mockResolvedValue({ columns: [], daily_rows: [], section: "new_users", total: null });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.history.pushState(null, "", "/");
|
||||
vi.useRealTimers();
|
||||
vi.clearAllMocks();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
test("keeps current data visible during scheduled refresh", async () => {
|
||||
@ -360,6 +364,119 @@ test("routes databi social page to Social BI and loads real overview rows", asyn
|
||||
expect(screen.getAllByText("$900").length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("renders social BI data requirements table with section filters and CSV export", async () => {
|
||||
window.history.pushState(null, "", "/databi/social/?view=table®ions=lalu:9,lalu:11");
|
||||
fetchSocialBiMaster.mockResolvedValue({
|
||||
access: { all: true, scopes: [] },
|
||||
apps: [{ app_code: "lalu", app_name: "Lalu", kind: "hyapp" }],
|
||||
operators: [],
|
||||
permissions: {},
|
||||
regions: []
|
||||
});
|
||||
fetchSocialBiOverview.mockResolvedValue({
|
||||
access: { all: true, scopes: [] },
|
||||
apps: [{ app_code: "lalu", app_name: "Lalu", daily_series: [], kind: "hyapp", total: {} }]
|
||||
});
|
||||
fetchSocialBiRequirements.mockImplementation(async ({ section }) => {
|
||||
if (section === "revenue") {
|
||||
return {
|
||||
access: { all: true, scopes: [] },
|
||||
apps: [{
|
||||
app_code: "lalu",
|
||||
app_name: "Lalu",
|
||||
kind: "hyapp",
|
||||
sections: [{
|
||||
columns: [
|
||||
{ key: "recharge_users", label: "充值用户", type: "count" },
|
||||
{ key: "recharge_usd_minor", label: "充值金额", type: "money_minor" }
|
||||
],
|
||||
daily_series: [{ metrics: { recharge_users: 2, recharge_usd_minor: 12300 }, stat_day: "2026-06-05" }],
|
||||
key: section,
|
||||
total: { label: "汇总", metrics: { recharge_users: 2, recharge_usd_minor: 12300 }, stat_day: "total" }
|
||||
}]
|
||||
}]
|
||||
};
|
||||
}
|
||||
return {
|
||||
access: { all: true, scopes: [] },
|
||||
apps: [{
|
||||
app_code: "lalu",
|
||||
app_name: "Lalu",
|
||||
kind: "hyapp",
|
||||
sections: [{
|
||||
columns: [
|
||||
{ key: "app_download_users", label: "下载APP人数", type: "count" },
|
||||
{ key: "registration_success_rate", label: "注册成功率", type: "ratio" }
|
||||
],
|
||||
daily_series: [{ metrics: { app_download_users: 12, registration_success_rate: 0.6666 }, stat_day: "2026-06-05" }],
|
||||
key: section,
|
||||
total: { label: "汇总", metrics: { app_download_users: 12, registration_success_rate: 0.6666 }, stat_day: "total" }
|
||||
}]
|
||||
}]
|
||||
};
|
||||
});
|
||||
|
||||
render(<DatabiApp />);
|
||||
await flushEffects();
|
||||
|
||||
const viewTabs = screen.getByRole("tablist", { name: "数据视图" });
|
||||
expect(within(viewTabs).getByRole("tab", { name: "数据明细" })).toHaveAttribute("aria-selected", "true");
|
||||
|
||||
await act(async () => {
|
||||
screen.getByRole("radio", { name: "数据需求" }).click();
|
||||
});
|
||||
await flushEffects();
|
||||
|
||||
expect(fetchSocialBiRequirements).toHaveBeenCalledWith(expect.objectContaining({
|
||||
payerType: "all",
|
||||
regionIds: [9, 11],
|
||||
section: "new_users",
|
||||
userRole: "all"
|
||||
}));
|
||||
expect(screen.getByRole("tab", { name: "P0新用户" })).toHaveAttribute("aria-selected", "true");
|
||||
expect(screen.getByText("下载APP人数")).toBeTruthy();
|
||||
expect(screen.getByText("注册成功率")).toBeTruthy();
|
||||
expect(screen.getAllByText("12").length).toBeGreaterThan(0);
|
||||
expect(screen.getAllByText("66.66%").length).toBeGreaterThan(0);
|
||||
expect(screen.queryByRole("radiogroup", { name: "用户身份" })).toBeNull();
|
||||
expect(screen.queryByRole("radiogroup", { name: "付费身份" })).toBeNull();
|
||||
|
||||
await act(async () => {
|
||||
screen.getByRole("tab", { name: "P1营收" }).click();
|
||||
});
|
||||
await flushEffects();
|
||||
expect(screen.getByRole("radiogroup", { name: "用户身份" })).toBeTruthy();
|
||||
expect(screen.getByRole("radiogroup", { name: "付费身份" })).toBeTruthy();
|
||||
|
||||
await act(async () => {
|
||||
screen.getByRole("radio", { name: "主播" }).click();
|
||||
});
|
||||
await flushEffects();
|
||||
await act(async () => {
|
||||
screen.getByRole("radio", { name: "已付费" }).click();
|
||||
});
|
||||
await flushEffects();
|
||||
|
||||
expect(fetchSocialBiRequirements).toHaveBeenLastCalledWith(expect.objectContaining({
|
||||
payerType: "paid",
|
||||
section: "revenue",
|
||||
userRole: "host"
|
||||
}));
|
||||
expect(screen.getByText("充值用户")).toBeTruthy();
|
||||
expect(screen.getByText("充值金额")).toBeTruthy();
|
||||
expect(screen.getAllByText("$123").length).toBeGreaterThan(0);
|
||||
|
||||
const urlMock = { createObjectURL: vi.fn(() => "blob:social-bi-requirements"), revokeObjectURL: vi.fn() };
|
||||
const anchorClick = vi.spyOn(HTMLAnchorElement.prototype, "click").mockImplementation(() => {});
|
||||
vi.stubGlobal("URL", urlMock);
|
||||
await act(async () => {
|
||||
screen.getByRole("button", { name: "导出 CSV" }).click();
|
||||
});
|
||||
expect(urlMock.createObjectURL).toHaveBeenCalledTimes(1);
|
||||
expect(anchorClick).toHaveBeenCalledTimes(1);
|
||||
anchorClick.mockRestore();
|
||||
});
|
||||
|
||||
test("renders social BI funnel view and limits funnel apps to supported apps", async () => {
|
||||
window.history.pushState(null, "", "/databi/social/?view=funnel");
|
||||
fetchSocialBiMaster.mockResolvedValue({
|
||||
|
||||
@ -180,6 +180,37 @@ export async function fetchSocialBiOverview({ appCodes, endMs, regionId, startMs
|
||||
return fetchDatabiData(apiEndpointPath(API_OPERATIONS.getSocialBiOverview), { query });
|
||||
}
|
||||
|
||||
export async function fetchSocialBiRequirements({ appCodes, endMs, payerType, regionId, regionIds, section, startMs, statTz, userRole }) {
|
||||
const query = {};
|
||||
if (statTz) {
|
||||
query.stat_tz = statTz;
|
||||
}
|
||||
if (startMs) {
|
||||
query.start_ms = String(startMs);
|
||||
}
|
||||
if (endMs) {
|
||||
query.end_ms = String(endMs);
|
||||
}
|
||||
if (appCodes?.length) {
|
||||
query.app_codes = appCodes.join(",");
|
||||
}
|
||||
if (regionId) {
|
||||
query.region_id = String(regionId);
|
||||
} else if (regionIds?.length) {
|
||||
query.region_ids = regionIds.join(",");
|
||||
}
|
||||
if (section) {
|
||||
query.section = section;
|
||||
}
|
||||
if (userRole) {
|
||||
query.user_role = userRole;
|
||||
}
|
||||
if (payerType) {
|
||||
query.payer_type = payerType;
|
||||
}
|
||||
return fetchDatabiData(apiEndpointPath(API_OPERATIONS.getSocialBiRequirements), { query });
|
||||
}
|
||||
|
||||
export async function fetchSocialBiFunnel({ appCodes, endMs, regionId, startMs, statTz }) {
|
||||
const query = {};
|
||||
if (statTz) {
|
||||
|
||||
@ -1,5 +1,13 @@
|
||||
import { afterEach, expect, test, vi } from "vitest";
|
||||
import { fetchFilterOptions, fetchSocialBiFilterOptions, fetchStatisticsOverview, getCurrentAppCode, getDefaultAppOptions, setCurrentAppCode } from "./api.js";
|
||||
import {
|
||||
fetchFilterOptions,
|
||||
fetchSocialBiFilterOptions,
|
||||
fetchSocialBiRequirements,
|
||||
fetchStatisticsOverview,
|
||||
getCurrentAppCode,
|
||||
getDefaultAppOptions,
|
||||
setCurrentAppCode
|
||||
} from "./api.js";
|
||||
|
||||
afterEach(() => {
|
||||
window.localStorage.clear();
|
||||
@ -87,6 +95,31 @@ test("loads social BI apps and operation users from admin APIs", async () => {
|
||||
expect(String(fetch.mock.calls[1][0])).toContain("status=active");
|
||||
});
|
||||
|
||||
test("loads social BI requirements with section role and payer filters", async () => {
|
||||
vi.stubGlobal("fetch", vi.fn(async () => jsonResponse({ section: "revenue", total: { recharge_users: 2 } })));
|
||||
|
||||
const data = await fetchSocialBiRequirements({
|
||||
appCodes: ["lalu", "huwaa"],
|
||||
endMs: 20,
|
||||
payerType: "paid",
|
||||
regionIds: [9, 11],
|
||||
section: "revenue",
|
||||
startMs: 10,
|
||||
statTz: "Asia/Shanghai",
|
||||
userRole: "host"
|
||||
});
|
||||
|
||||
const requestURL = new URL(String(fetch.mock.calls[0][0]));
|
||||
expect(requestURL.pathname).toBe("/api/v1/admin/databi/social/requirements");
|
||||
expect(requestURL.searchParams.get("app_codes")).toBe("lalu,huwaa");
|
||||
expect(requestURL.searchParams.get("section")).toBe("revenue");
|
||||
expect(requestURL.searchParams.get("user_role")).toBe("host");
|
||||
expect(requestURL.searchParams.get("payer_type")).toBe("paid");
|
||||
expect(requestURL.searchParams.get("region_ids")).toBe("9,11");
|
||||
expect(requestURL.searchParams.get("stat_tz")).toBe("Asia/Shanghai");
|
||||
expect(data.total.recharge_users).toBe(2);
|
||||
});
|
||||
|
||||
test("omits app scope headers and query params for all databi apps", async () => {
|
||||
vi.stubGlobal("fetch", vi.fn(async () => jsonResponse({ updated_at_ms: 1 })));
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
// 视图不直接调 API,只消费这里的产物(原始 snake_case 指标行 + 维度字段)。
|
||||
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { fetchSocialBiFunnel, fetchSocialBiKpi, fetchSocialBiMaster, fetchSocialBiOverview } from "../api.js";
|
||||
import { fetchSocialBiFunnel, fetchSocialBiKpi, fetchSocialBiMaster, fetchSocialBiOverview, fetchSocialBiRequirements } from "../api.js";
|
||||
import { DEFAULT_TIME_ZONE, rangeEndMs, rangeStartMs } from "../utils/time.js";
|
||||
import { ALL, appSelectionMatches, regionSelectionMatches, resolveDateRange } from "./state.js";
|
||||
|
||||
@ -16,10 +16,14 @@ export function useSocialBiData(filters) {
|
||||
const [overview, setOverview] = useState(null);
|
||||
const [funnel, setFunnel] = useState(null);
|
||||
const [kpi, setKpi] = useState(null);
|
||||
const [requirements, setRequirements] = useState(null);
|
||||
const [requirementsError, setRequirementsError] = useState("");
|
||||
const [isRequirementsLoading, setIsRequirementsLoading] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [errors, setErrors] = useState([]);
|
||||
const [refreshToken, setRefreshToken] = useState(0);
|
||||
const requestSeq = useRef(0);
|
||||
const requirementsSeq = useRef(0);
|
||||
|
||||
useEffect(() => {
|
||||
let ignore = false;
|
||||
@ -119,6 +123,58 @@ export function useSocialBiData(filters) {
|
||||
});
|
||||
}, [appCodes, funnelAppCodes, isMasterSettled, range, refreshToken]);
|
||||
|
||||
const loadRequirements = useCallback(
|
||||
({ payerType, section, userRole } = {}) => {
|
||||
if (!isMasterSettled) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
const sequence = ++requirementsSeq.current;
|
||||
const startMs = rangeStartMs(range, SOCIAL_BI_TZ);
|
||||
const endMs = rangeEndMs(range, SOCIAL_BI_TZ);
|
||||
const requestScopes = buildRequirementRequestScopes(appCodes, filters.regions);
|
||||
setIsRequirementsLoading(true);
|
||||
setRequirementsError("");
|
||||
setRequirements(null);
|
||||
if (!requestScopes.length) {
|
||||
const emptyResult = { access: overview?.access || null, apps: [] };
|
||||
setRequirements(emptyResult);
|
||||
setIsRequirementsLoading(false);
|
||||
return Promise.resolve(emptyResult);
|
||||
}
|
||||
// 数据需求按表格页局部 section/角色/付费筛选请求;全局 App/日期仍复用 Social BI 顶栏口径。
|
||||
return Promise.all(requestScopes.map((scope) => fetchSocialBiRequirements({
|
||||
...scope,
|
||||
endMs,
|
||||
payerType,
|
||||
section,
|
||||
startMs,
|
||||
statTz: SOCIAL_BI_TZ,
|
||||
userRole
|
||||
})))
|
||||
.then((results) => {
|
||||
if (sequence !== requirementsSeq.current) {
|
||||
return null;
|
||||
}
|
||||
const data = mergeRequirementResponses(results);
|
||||
setRequirements(data);
|
||||
return data;
|
||||
})
|
||||
.catch((error) => {
|
||||
if (sequence === requirementsSeq.current) {
|
||||
setRequirements(null);
|
||||
setRequirementsError(error?.message || "数据需求加载失败");
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.finally(() => {
|
||||
if (sequence === requirementsSeq.current) {
|
||||
setIsRequirementsLoading(false);
|
||||
}
|
||||
});
|
||||
},
|
||||
[appCodes, filters.regions, isMasterSettled, overview?.access, range]
|
||||
);
|
||||
|
||||
const refresh = useCallback(() => {
|
||||
setRefreshToken((current) => current + 1);
|
||||
}, []);
|
||||
@ -132,10 +188,15 @@ export function useSocialBiData(filters) {
|
||||
funnel,
|
||||
funnelAppCodes,
|
||||
isLoading,
|
||||
isRequirementsLoading,
|
||||
kpi,
|
||||
loadRequirements,
|
||||
master,
|
||||
overview,
|
||||
range,
|
||||
refreshToken,
|
||||
requirements,
|
||||
requirementsError,
|
||||
refresh
|
||||
};
|
||||
}
|
||||
@ -236,3 +297,61 @@ function buildRegionByCountryIndex(master) {
|
||||
});
|
||||
return index;
|
||||
}
|
||||
|
||||
function buildRequirementRequestScopes(appCodes, regions) {
|
||||
const selectedApps = new Set((appCodes || []).filter(Boolean));
|
||||
if (!regions?.length || regions.includes(ALL)) {
|
||||
return [{ appCodes: [...selectedApps] }];
|
||||
}
|
||||
const byApp = new Map();
|
||||
regions.forEach((selection) => {
|
||||
const value = String(selection || "").trim();
|
||||
if (!value || value === ALL) {
|
||||
return;
|
||||
}
|
||||
if (value.startsWith("app:")) {
|
||||
const appCode = value.slice(4).trim().toLowerCase();
|
||||
if (appCode) {
|
||||
byApp.set(appCode, null);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const [rawAppCode, rawRegionID] = value.split(":");
|
||||
const appCode = String(rawAppCode || "").trim().toLowerCase();
|
||||
const regionID = Number(rawRegionID);
|
||||
if (!appCode || !Number.isFinite(regionID) || regionID <= 0 || byApp.get(appCode) === null) {
|
||||
return;
|
||||
}
|
||||
if (!byApp.has(appCode)) {
|
||||
byApp.set(appCode, new Set());
|
||||
}
|
||||
byApp.get(appCode).add(regionID);
|
||||
});
|
||||
const scopes = [];
|
||||
byApp.forEach((regionIDs, appCode) => {
|
||||
if (selectedApps.size && !selectedApps.has(appCode)) {
|
||||
return;
|
||||
}
|
||||
if (regionIDs === null) {
|
||||
scopes.push({ appCodes: [appCode] });
|
||||
return;
|
||||
}
|
||||
const ids = [...regionIDs].sort((left, right) => left - right);
|
||||
if (ids.length === 1) {
|
||||
scopes.push({ appCodes: [appCode], regionId: ids[0] });
|
||||
} else if (ids.length > 1) {
|
||||
scopes.push({ appCodes: [appCode], regionIds: ids });
|
||||
}
|
||||
});
|
||||
return scopes;
|
||||
}
|
||||
|
||||
function mergeRequirementResponses(results) {
|
||||
const merged = { access: results.find((item) => item?.access)?.access || null, apps: [] };
|
||||
results.forEach((item) => {
|
||||
if (Array.isArray(item?.apps)) {
|
||||
merged.apps.push(...item.apps);
|
||||
}
|
||||
});
|
||||
return merged;
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// DataTableView:数据明细视图 —— Excel 式全字段宽表(维度切换 / 列组开关 / 指标排序 /
|
||||
// 首列冻结 / CSV 导出)。行数据保持后端原始 snake_case 口径,渲染时经 metrics.js 统一格式化。
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import Button from "@mui/material/Button";
|
||||
import FileDownloadOutlined from "@mui/icons-material/FileDownloadOutlined";
|
||||
import { useSocialBi } from "../SocialBiApp.jsx";
|
||||
@ -13,7 +13,7 @@ import {
|
||||
metricLabel,
|
||||
metricTooltip
|
||||
} from "../metrics.js";
|
||||
import { downloadCsv, isBlank } from "../format.js";
|
||||
import { downloadCsv, formatCount, formatDurationMs, formatMoneyMinor, isBlank } from "../format.js";
|
||||
import { rangeLabel } from "../state.js";
|
||||
import "./data-table-view.css";
|
||||
|
||||
@ -39,6 +39,104 @@ const DIMENSIONS = [
|
||||
{ csvName: "country", dims: ["app", "country"], hasDay: false, key: "countryTotals", label: "国家" }
|
||||
];
|
||||
|
||||
const TABLE_MODES = [
|
||||
{ key: "wide", label: "运营宽表" },
|
||||
{ key: "requirements", label: "数据需求" }
|
||||
];
|
||||
|
||||
const REQUIREMENT_SECTIONS = [
|
||||
{
|
||||
csvName: "p0-new-users",
|
||||
fallbackColumns: [
|
||||
{ key: "app_first_open_users", label: "下载APP人数", type: "count" },
|
||||
{ key: "new_users", label: "新增注册", type: "count" },
|
||||
{ key: "profile_complete_users", label: "完善资料", type: "count" },
|
||||
{ key: "first_room_join_users", label: "首次进房", type: "count" }
|
||||
],
|
||||
key: "new_users",
|
||||
label: "P0新用户"
|
||||
},
|
||||
{
|
||||
csvName: "p1-revenue",
|
||||
fallbackColumns: [
|
||||
{ key: "recharge_usd_minor", label: "充值", type: "money_minor" },
|
||||
{ key: "recharge_users", label: "充值用户", type: "count" },
|
||||
{ key: "first_recharge_users", label: "首充用户", type: "count" },
|
||||
{ key: "repeat_recharge_users", label: "复购用户", type: "count" },
|
||||
{ key: "arppu_usd_minor", label: "ARPPU", type: "money_minor" }
|
||||
],
|
||||
key: "revenue",
|
||||
label: "P1营收",
|
||||
payerFilter: true,
|
||||
roleFilter: true
|
||||
},
|
||||
{
|
||||
csvName: "p2-retention",
|
||||
fallbackColumns: [
|
||||
{ key: "cohort_users", label: "留存基数", type: "count" },
|
||||
{ key: "d1_retention_users", label: "次留用户", type: "count" },
|
||||
{ key: "d1_retention_rate", label: "次留率", type: "ratio" },
|
||||
{ key: "d7_retention_rate", label: "7日留存", type: "ratio" },
|
||||
{ key: "d30_retention_rate", label: "30日留存", type: "ratio" }
|
||||
],
|
||||
key: "retention",
|
||||
label: "P2留存",
|
||||
payerFilter: true,
|
||||
roleFilter: true
|
||||
},
|
||||
{
|
||||
csvName: "p2-active",
|
||||
fallbackColumns: [
|
||||
{ key: "active_users", label: "活跃用户", type: "count" },
|
||||
{ key: "room_join_users", label: "进房用户", type: "count" },
|
||||
{ key: "mic_users", label: "上麦用户", type: "count" },
|
||||
{ key: "message_users", label: "发言用户", type: "count" },
|
||||
{ key: "avg_session_ms", label: "人均在线", type: "duration_ms" }
|
||||
],
|
||||
key: "active",
|
||||
label: "P2活跃",
|
||||
payerFilter: true,
|
||||
roleFilter: true
|
||||
},
|
||||
{
|
||||
csvName: "p4-gift",
|
||||
fallbackColumns: [
|
||||
{ key: "gift_panel_open_users", label: "礼物面板用户", type: "count" },
|
||||
{ key: "gift_senders", label: "送礼用户", type: "count" },
|
||||
{ key: "gift_coin_spent", label: "礼物流水", type: "coin" },
|
||||
{ key: "gift_paid_users", label: "付费送礼用户", type: "count" }
|
||||
],
|
||||
key: "gift",
|
||||
label: "P4送礼",
|
||||
roleFilter: true
|
||||
},
|
||||
{
|
||||
csvName: "p5-game",
|
||||
fallbackColumns: [
|
||||
{ key: "probability_game_panel_open_users", label: "概率游戏面板用户", type: "count" },
|
||||
{ key: "game_players", label: "押注用户", type: "count" },
|
||||
{ key: "game_turnover", label: "游戏流水", type: "coin" },
|
||||
{ key: "game_profit", label: "游戏利润", type: "coin" },
|
||||
{ key: "game_profit_rate", label: "游戏利润率", type: "ratio" }
|
||||
],
|
||||
key: "game",
|
||||
label: "P5游戏",
|
||||
roleFilter: true
|
||||
}
|
||||
];
|
||||
|
||||
const USER_ROLE_OPTIONS = [
|
||||
{ label: "全部角色", value: "all" },
|
||||
{ label: "主播", value: "host" },
|
||||
{ label: "用户", value: "user" }
|
||||
];
|
||||
|
||||
const PAYER_TYPE_OPTIONS = [
|
||||
{ label: "全部付费", value: "all" },
|
||||
{ label: "已付费", value: "paid" },
|
||||
{ label: "未付费", value: "unpaid" }
|
||||
];
|
||||
|
||||
function readClosedGroups() {
|
||||
try {
|
||||
const raw = window.localStorage.getItem(GROUPS_STORAGE_KEY);
|
||||
@ -89,16 +187,215 @@ function rowIdentity(row, dimKey, index) {
|
||||
return parts.length ? `${dimKey}:${parts.join("|")}` : `${dimKey}:${index}`;
|
||||
}
|
||||
|
||||
function requirementSectionConfig(key) {
|
||||
return REQUIREMENT_SECTIONS.find((section) => section.key === key) || REQUIREMENT_SECTIONS[0];
|
||||
}
|
||||
|
||||
function asArray(value) {
|
||||
return Array.isArray(value) ? value : [];
|
||||
}
|
||||
|
||||
function sectionMatches(item, sectionKey) {
|
||||
return String(item?.section ?? item?.key ?? item?.code ?? "") === sectionKey;
|
||||
}
|
||||
|
||||
function normalizeRequirementColumn(column) {
|
||||
const key = String(column?.key ?? column?.field ?? column?.name ?? "").trim();
|
||||
if (!key) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
key,
|
||||
label: column.label || column.title || column.name || key,
|
||||
tooltip: column.tooltip || column.description || column.remark || "",
|
||||
type: column.type || column.value_type || column.format || ""
|
||||
};
|
||||
}
|
||||
|
||||
function requirementAppContext(app) {
|
||||
return {
|
||||
app_code: app?.app_code || app?.appCode || "",
|
||||
app_name: app?.app_name || app?.appName || app?.app_code || app?.appCode || "",
|
||||
kind: app?.kind || "",
|
||||
restricted: Boolean(app?.restricted)
|
||||
};
|
||||
}
|
||||
|
||||
function flattenRequirementRow(row, context = {}) {
|
||||
if (!row || typeof row !== "object") {
|
||||
return null;
|
||||
}
|
||||
const metrics = row.metrics && typeof row.metrics === "object" && !Array.isArray(row.metrics) ? row.metrics : {};
|
||||
const flattened = { ...context, ...row, ...metrics };
|
||||
delete flattened.metrics;
|
||||
return flattened;
|
||||
}
|
||||
|
||||
function normalizedRequirementColumns(sectionItem, fallbackColumns) {
|
||||
const columns = asArray(sectionItem?.columns || sectionItem?.fields || sectionItem?.metrics).map(normalizeRequirementColumn).filter(Boolean);
|
||||
return columns.length ? columns : fallbackColumns;
|
||||
}
|
||||
|
||||
function normalizeRequirementPayload(payload, sectionKey) {
|
||||
const fallbackColumns = requirementSectionConfig(sectionKey).fallbackColumns;
|
||||
const appSections = asArray(payload?.apps).flatMap((app) => {
|
||||
const context = requirementAppContext(app);
|
||||
return asArray(app?.sections)
|
||||
.filter((section) => sectionMatches(section, sectionKey))
|
||||
.map((section) => ({ context, section }));
|
||||
});
|
||||
if (appSections.length) {
|
||||
const firstSection = appSections.find(({ section }) => asArray(section?.columns || section?.fields || section?.metrics).length)?.section
|
||||
|| appSections[0].section;
|
||||
const columns = normalizedRequirementColumns(firstSection, fallbackColumns);
|
||||
if (appSections.length === 1) {
|
||||
const { context, section } = appSections[0];
|
||||
return {
|
||||
columns,
|
||||
rows: asArray(section.daily_rows || section.dailyRows || section.daily_series || section.rows || section.items)
|
||||
.map((row) => flattenRequirementRow(row, context))
|
||||
.filter(Boolean),
|
||||
total: flattenRequirementRow(section.total || section.totals || section.summary, context)
|
||||
};
|
||||
}
|
||||
const rows = appSections.flatMap(({ context, section }) => {
|
||||
const total = flattenRequirementRow(section.total || section.totals || section.summary, { ...context, stat_day: "汇总" });
|
||||
const dailyRows = asArray(section.daily_rows || section.dailyRows || section.daily_series || section.rows || section.items)
|
||||
.map((row) => flattenRequirementRow(row, context))
|
||||
.filter(Boolean);
|
||||
return total ? [total, ...dailyRows] : dailyRows;
|
||||
});
|
||||
return { columns, rows, total: null };
|
||||
}
|
||||
|
||||
const sectionItem = asArray(payload?.sections).find((item) => sectionMatches(item, sectionKey))
|
||||
|| asArray(payload?.items).find((item) => sectionMatches(item, sectionKey) && (item.columns || item.daily_rows || item.dailyRows || item.total || item.totals))
|
||||
|| payload
|
||||
|| {};
|
||||
const columns = normalizedRequirementColumns(sectionItem, fallbackColumns);
|
||||
const rows = asArray(sectionItem.daily_rows || sectionItem.dailyRows || sectionItem.daily_series || sectionItem.rows || sectionItem.items)
|
||||
.map((row) => flattenRequirementRow(row))
|
||||
.filter(Boolean);
|
||||
const total = flattenRequirementRow(sectionItem.total || sectionItem.totals || sectionItem.summary);
|
||||
return {
|
||||
columns,
|
||||
rows,
|
||||
total
|
||||
};
|
||||
}
|
||||
|
||||
function inferRequirementType(column) {
|
||||
const type = String(column.type || "").toLowerCase();
|
||||
const key = String(column.key || "").toLowerCase();
|
||||
if (type.includes("money") || key.endsWith("_usd_minor")) {
|
||||
return "money_minor";
|
||||
}
|
||||
if (type.includes("ratio") || type.includes("rate") || type.includes("percent") || key.endsWith("_rate")) {
|
||||
return "ratio";
|
||||
}
|
||||
if (type.includes("duration") || key.endsWith("_ms")) {
|
||||
return "duration_ms";
|
||||
}
|
||||
if (type.includes("coin") || key.endsWith("_coin") || key.endsWith("_turnover") || key.endsWith("_profit")) {
|
||||
return "coin";
|
||||
}
|
||||
return type || "number";
|
||||
}
|
||||
|
||||
function truncateTwoDecimals(value) {
|
||||
const numeric = Number(value);
|
||||
if (!Number.isFinite(numeric)) {
|
||||
return "--";
|
||||
}
|
||||
const truncated = Math.trunc(numeric * 100) / 100;
|
||||
return truncated.toLocaleString("en-US", { maximumFractionDigits: 2 });
|
||||
}
|
||||
|
||||
function formatRequirementRatio(value) {
|
||||
const numeric = Number(value);
|
||||
if (!Number.isFinite(numeric)) {
|
||||
return "--";
|
||||
}
|
||||
return `${truncateTwoDecimals(numeric * 100)}%`;
|
||||
}
|
||||
|
||||
function formatRequirementValue(column, value) {
|
||||
if (isBlank(value)) {
|
||||
return "--";
|
||||
}
|
||||
const type = inferRequirementType(column);
|
||||
if (type === "money_minor") {
|
||||
return formatMoneyMinor(value);
|
||||
}
|
||||
if (type === "ratio") {
|
||||
return formatRequirementRatio(value);
|
||||
}
|
||||
if (type === "duration_ms") {
|
||||
return formatDurationMs(value);
|
||||
}
|
||||
if (type === "coin" || type === "count" || type === "integer") {
|
||||
return formatCount(value);
|
||||
}
|
||||
const numeric = Number(value);
|
||||
if (Number.isFinite(numeric)) {
|
||||
return Number.isInteger(numeric) ? formatCount(numeric) : truncateTwoDecimals(numeric);
|
||||
}
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function csvRequirementValue(column, value) {
|
||||
if (isBlank(value)) {
|
||||
return "";
|
||||
}
|
||||
const type = inferRequirementType(column);
|
||||
if (type === "money_minor") {
|
||||
return Number(value) / 100;
|
||||
}
|
||||
if (type === "ratio") {
|
||||
return Number(value) * 100;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function requirementRowIdentity(row, sectionKey, index) {
|
||||
const parts = [row.app_code, row.stat_day, row.region_id].filter((part) => part !== undefined && part !== null && part !== "");
|
||||
return parts.length ? `${sectionKey}:${parts.join("|")}` : `${sectionKey}:${index}`;
|
||||
}
|
||||
|
||||
function requirementAppName(row) {
|
||||
return row?.app_name || row?.app_code || "全部";
|
||||
}
|
||||
|
||||
export function DataTableView() {
|
||||
const { derived, isLoading, range } = useSocialBi();
|
||||
const {
|
||||
derived,
|
||||
isLoading,
|
||||
isRequirementsLoading,
|
||||
loadRequirements,
|
||||
range,
|
||||
refreshToken,
|
||||
requirements,
|
||||
requirementsError
|
||||
} = useSocialBi();
|
||||
const [mode, setMode] = useState("wide");
|
||||
const [dimKey, setDimKey] = useState("appDaily");
|
||||
const [closedGroups, setClosedGroups] = useState(readClosedGroups);
|
||||
const [sort, setSort] = useState(null);
|
||||
const [requirementSection, setRequirementSection] = useState(REQUIREMENT_SECTIONS[0].key);
|
||||
const [requirementRole, setRequirementRole] = useState("all");
|
||||
const [requirementPayer, setRequirementPayer] = useState("all");
|
||||
|
||||
const dim = DIMENSIONS.find((item) => item.key === dimKey) || DIMENSIONS[0];
|
||||
const dimColumns = dim.dims.map((name) => DIM_COLUMNS[name]);
|
||||
const hasDay = dim.hasDay;
|
||||
const baseRows = derived?.[dim.key] || EMPTY_ROWS;
|
||||
const activeRequirementSection = requirementSectionConfig(requirementSection);
|
||||
const effectiveRequirementRole = activeRequirementSection.roleFilter ? requirementRole : "all";
|
||||
const effectiveRequirementPayer = activeRequirementSection.payerFilter ? requirementPayer : "all";
|
||||
const requirementData = useMemo(() => normalizeRequirementPayload(requirements, requirementSection), [requirements, requirementSection]);
|
||||
const requirementColumns = requirementData.columns;
|
||||
const requirementRows = requirementData.rows;
|
||||
const requirementTotal = requirementData.total;
|
||||
|
||||
const visibleGroups = useMemo(() => METRIC_GROUPS.filter((group) => !closedGroups.includes(group.key)), [closedGroups]);
|
||||
const visibleMetrics = useMemo(() => visibleGroups.flatMap((group) => group.metrics), [visibleGroups]);
|
||||
@ -129,11 +426,33 @@ export function DataTableView() {
|
||||
const hasRestricted = baseRows.some((row) => row.restricted);
|
||||
const columnCount = dimColumns.length + visibleMetrics.length;
|
||||
|
||||
useEffect(() => {
|
||||
if (mode !== "requirements") {
|
||||
return;
|
||||
}
|
||||
loadRequirements({
|
||||
payerType: effectiveRequirementPayer,
|
||||
section: requirementSection,
|
||||
userRole: effectiveRequirementRole
|
||||
});
|
||||
}, [effectiveRequirementPayer, effectiveRequirementRole, loadRequirements, mode, refreshToken, requirementSection]);
|
||||
|
||||
const handleDimChange = (key) => {
|
||||
setDimKey(key);
|
||||
setSort(null);
|
||||
};
|
||||
|
||||
const handleRequirementSectionChange = (key) => {
|
||||
const nextSection = requirementSectionConfig(key);
|
||||
setRequirementSection(nextSection.key);
|
||||
if (!nextSection.roleFilter) {
|
||||
setRequirementRole("all");
|
||||
}
|
||||
if (!nextSection.payerFilter) {
|
||||
setRequirementPayer("all");
|
||||
}
|
||||
};
|
||||
|
||||
const toggleGroup = (key) => {
|
||||
const next = closedGroups.includes(key) ? closedGroups.filter((item) => item !== key) : [...closedGroups, key];
|
||||
setClosedGroups(next);
|
||||
@ -157,9 +476,22 @@ export function DataTableView() {
|
||||
downloadCsv(`social-bi-${dim.csvName}-${rangeLabel(range).replace(/\s+/g, "")}.csv`, header, rows);
|
||||
};
|
||||
|
||||
let content;
|
||||
const handleRequirementsExport = () => {
|
||||
const header = ["日期", "App", ...requirementColumns.map((column) => column.label)];
|
||||
const rows = [
|
||||
...(requirementTotal ? [{ ...requirementTotal, app_name: "全部", stat_day: "汇总" }] : []),
|
||||
...requirementRows
|
||||
].map((row) => [
|
||||
row.stat_day || "--",
|
||||
requirementAppName(row),
|
||||
...requirementColumns.map((column) => csvRequirementValue(column, row[column.key]))
|
||||
]);
|
||||
downloadCsv(`social-bi-requirements-${activeRequirementSection.csvName}-${rangeLabel(range).replace(/\s+/g, "")}.csv`, header, rows);
|
||||
};
|
||||
|
||||
let wideContent;
|
||||
if (isLoading && !baseRows.length) {
|
||||
content = (
|
||||
wideContent = (
|
||||
<div className="sbi-dt-skeleton" aria-label="数据加载中">
|
||||
{SKELETON_WIDTHS.map((width) => (
|
||||
<span className="sbi-skeleton" key={width} style={{ width }} />
|
||||
@ -167,25 +499,19 @@ export function DataTableView() {
|
||||
</div>
|
||||
);
|
||||
} else if (!baseRows.length) {
|
||||
content = (
|
||||
wideContent = (
|
||||
<div className="sbi-empty">
|
||||
<strong>当前维度暂无数据</strong>
|
||||
<span>
|
||||
{dim.dims.includes("region")
|
||||
? "该区间没有区域级数据,可能尚未配置区域目录;试试更换日期区间或切换其它维度"
|
||||
: "试试更换日期区间、调整 App 筛选或切换其它维度"}
|
||||
</span>
|
||||
<strong>当前无数据</strong>
|
||||
</div>
|
||||
);
|
||||
} else if (!visibleMetrics.length) {
|
||||
content = (
|
||||
wideContent = (
|
||||
<div className="sbi-empty">
|
||||
<strong>所有列组已隐藏</strong>
|
||||
<span>点击上方列组开关,恢复需要查看的指标列</span>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
content = (
|
||||
wideContent = (
|
||||
<div className="sbi-table-scroll sbi-dt-scroll">
|
||||
<table className="sbi-table sbi-dt-table">
|
||||
<thead>
|
||||
@ -258,15 +584,100 @@ export function DataTableView() {
|
||||
);
|
||||
}
|
||||
|
||||
let requirementsContent;
|
||||
if (isRequirementsLoading && !requirementRows.length && !requirementTotal) {
|
||||
requirementsContent = (
|
||||
<div className="sbi-dt-skeleton" aria-label="数据需求加载中">
|
||||
{SKELETON_WIDTHS.map((width) => (
|
||||
<span className="sbi-skeleton" key={width} style={{ width }} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
} else if (requirementsError) {
|
||||
requirementsContent = (
|
||||
<div className="sbi-empty" role="alert">
|
||||
<strong>{requirementsError}</strong>
|
||||
</div>
|
||||
);
|
||||
} else if (!requirementRows.length && !requirementTotal) {
|
||||
requirementsContent = (
|
||||
<div className="sbi-empty">
|
||||
<strong>当前无数据</strong>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
requirementsContent = (
|
||||
<div className="sbi-table-scroll sbi-dt-scroll">
|
||||
<table className="sbi-table sbi-dt-req-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="is-left sbi-dt-sticky">日期</th>
|
||||
<th className="is-left">App</th>
|
||||
{requirementColumns.map((column) => (
|
||||
<th key={column.key}>
|
||||
<span className={column.tooltip ? "sbi-metric-hint" : ""} title={column.tooltip || undefined}>
|
||||
{column.label}
|
||||
</span>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{requirementTotal ? (
|
||||
<tr className="is-total">
|
||||
<td className="is-left sbi-dt-sticky">汇总</td>
|
||||
<td className="is-left">全部</td>
|
||||
{requirementColumns.map((column) => (
|
||||
<td key={column.key}>{formatRequirementValue(column, requirementTotal[column.key])}</td>
|
||||
))}
|
||||
</tr>
|
||||
) : null}
|
||||
{requirementRows.map((row, rowIndex) => (
|
||||
<tr key={requirementRowIdentity(row, activeRequirementSection.key, rowIndex)}>
|
||||
<td className="is-left sbi-dt-sticky">{row.stat_day || "--"}</td>
|
||||
<td className="is-left">{requirementAppName(row)}</td>
|
||||
{requirementColumns.map((column) => (
|
||||
<td key={column.key}>{formatRequirementValue(column, row[column.key])}</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const rowCount = mode === "requirements" ? requirementRows.length : sortedRows.length;
|
||||
const content = mode === "requirements" ? requirementsContent : wideContent;
|
||||
const exportDisabled = mode === "requirements"
|
||||
? (!requirementRows.length && !requirementTotal) || isRequirementsLoading
|
||||
: !sortedRows.length || !visibleMetrics.length;
|
||||
|
||||
return (
|
||||
<section className="sbi-card">
|
||||
<div className="sbi-card-header">
|
||||
<strong>数据明细</strong>
|
||||
<small>
|
||||
{rangeLabel(range)} · 共 {sortedRows.length} 行
|
||||
{rangeLabel(range)} · 共 {rowCount} 行
|
||||
</small>
|
||||
</div>
|
||||
<div className="sbi-dt-tools">
|
||||
<div className="sbi-seg" role="radiogroup" aria-label="数据明细模式">
|
||||
{TABLE_MODES.map((item) => (
|
||||
<button
|
||||
aria-checked={mode === item.key}
|
||||
className={mode === item.key ? "is-active" : ""}
|
||||
key={item.key}
|
||||
onClick={() => setMode(item.key)}
|
||||
role="radio"
|
||||
type="button"
|
||||
>
|
||||
{item.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{mode === "wide" ? (
|
||||
<>
|
||||
<div className="sbi-seg" role="radiogroup" aria-label="明细维度">
|
||||
{DIMENSIONS.map((item) => (
|
||||
<button
|
||||
@ -300,10 +711,47 @@ export function DataTableView() {
|
||||
})}
|
||||
</div>
|
||||
{hasRestricted ? <span className="sbi-badge">已按数据范围过滤</span> : null}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="sbi-dt-section-tabs" role="tablist" aria-label="数据需求分组">
|
||||
{REQUIREMENT_SECTIONS.map((section) => (
|
||||
<button
|
||||
aria-selected={activeRequirementSection.key === section.key}
|
||||
className={activeRequirementSection.key === section.key ? "is-active" : ""}
|
||||
key={section.key}
|
||||
onClick={() => handleRequirementSectionChange(section.key)}
|
||||
role="tab"
|
||||
type="button"
|
||||
>
|
||||
{section.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{activeRequirementSection.roleFilter ? (
|
||||
<RequirementFilterGroup
|
||||
ariaLabel="用户身份"
|
||||
label="用户身份"
|
||||
options={USER_ROLE_OPTIONS}
|
||||
value={effectiveRequirementRole}
|
||||
onChange={setRequirementRole}
|
||||
/>
|
||||
) : null}
|
||||
{activeRequirementSection.payerFilter ? (
|
||||
<RequirementFilterGroup
|
||||
ariaLabel="付费身份"
|
||||
label="付费身份"
|
||||
options={PAYER_TYPE_OPTIONS}
|
||||
value={effectiveRequirementPayer}
|
||||
onChange={setRequirementPayer}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
<div className="sbi-dt-export">
|
||||
<Button
|
||||
disabled={!sortedRows.length || !visibleMetrics.length}
|
||||
onClick={handleExport}
|
||||
disabled={exportDisabled}
|
||||
onClick={mode === "requirements" ? handleRequirementsExport : handleExport}
|
||||
size="small"
|
||||
startIcon={<FileDownloadOutlined />}
|
||||
variant="outlined"
|
||||
@ -316,3 +764,25 @@ export function DataTableView() {
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function RequirementFilterGroup({ ariaLabel, label, onChange, options, value }) {
|
||||
return (
|
||||
<div className="sbi-dt-filter-group">
|
||||
<span className="sbi-dt-groups-label">{label}</span>
|
||||
<div className="sbi-seg" role="radiogroup" aria-label={ariaLabel}>
|
||||
{options.map((option) => (
|
||||
<button
|
||||
aria-checked={value === option.value}
|
||||
className={value === option.value ? "is-active" : ""}
|
||||
key={option.value}
|
||||
onClick={() => onChange(option.value)}
|
||||
role="radio"
|
||||
type="button"
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -16,6 +16,37 @@
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.sbi-dt-section-tabs {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.sbi-dt-section-tabs > button {
|
||||
padding: 6px 12px;
|
||||
border: 1px solid var(--sbi-border);
|
||||
border-radius: 999px;
|
||||
background: #ffffff;
|
||||
color: var(--sbi-text-2);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.sbi-dt-section-tabs > button.is-active {
|
||||
border-color: var(--sbi-accent);
|
||||
color: var(--sbi-accent);
|
||||
box-shadow: 0 0 0 3px rgba(37, 87, 214, 0.1);
|
||||
}
|
||||
|
||||
.sbi-dt-filter-group {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.sbi-dt-groups-label {
|
||||
color: var(--sbi-text-3);
|
||||
font-size: 12px;
|
||||
@ -57,24 +88,32 @@
|
||||
color: var(--sbi-accent);
|
||||
}
|
||||
|
||||
.sbi-dt-req-table th:first-child,
|
||||
.sbi-dt-req-table td:first-child {
|
||||
min-width: 104px;
|
||||
}
|
||||
|
||||
.sbi-dt-sort {
|
||||
margin-left: 3px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
/* 首列冻结:白底 + 右侧阴影;合计行/悬浮行的底色由 social-v2.css 中更高优先级规则接管。 */
|
||||
.sbi-dt-table .sbi-dt-sticky {
|
||||
.sbi-dt-table .sbi-dt-sticky,
|
||||
.sbi-dt-req-table .sbi-dt-sticky {
|
||||
position: sticky;
|
||||
left: 0;
|
||||
box-shadow: 6px 0 10px -8px rgba(16, 34, 56, 0.28);
|
||||
}
|
||||
|
||||
.sbi-dt-table td.sbi-dt-sticky {
|
||||
.sbi-dt-table td.sbi-dt-sticky,
|
||||
.sbi-dt-req-table td.sbi-dt-sticky {
|
||||
z-index: 1;
|
||||
background: var(--sbi-card);
|
||||
}
|
||||
|
||||
.sbi-dt-table th.sbi-dt-sticky {
|
||||
.sbi-dt-table th.sbi-dt-sticky,
|
||||
.sbi-dt-req-table th.sbi-dt-sticky {
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
|
||||
@ -119,7 +119,7 @@ test("coin seller ledger API uses generated admin path and filters", async () =>
|
||||
);
|
||||
|
||||
await listCoinSellerLedger({
|
||||
ledger_type: "seller_transfer",
|
||||
ledger_type: "sub_seller_transfer",
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
seller_user_id: "3001",
|
||||
@ -129,7 +129,7 @@ test("coin seller ledger API uses generated admin path and filters", async () =>
|
||||
|
||||
expect(String(listUrl)).toContain("/api/v1/admin/operations/coin-seller-ledger?");
|
||||
expect(String(listUrl)).toContain("seller_user_id=3001");
|
||||
expect(String(listUrl)).toContain("ledger_type=seller_transfer");
|
||||
expect(String(listUrl)).toContain("ledger_type=sub_seller_transfer");
|
||||
expect(listInit?.method).toBe("GET");
|
||||
});
|
||||
|
||||
|
||||
@ -141,8 +141,9 @@ export function listCoinSellerLedger(query: PageQuery = {}): Promise<ApiPage<Coi
|
||||
}
|
||||
|
||||
export function exportCoinSellerLedger(query: PageQuery = {}): Promise<Response> {
|
||||
return apiRequest("/v1/admin/operations/coin-seller-ledger/export", {
|
||||
method: "GET",
|
||||
const endpoint = API_ENDPOINTS.exportCoinSellerLedger;
|
||||
return apiRequest(apiEndpointPath(API_OPERATIONS.exportCoinSellerLedger), {
|
||||
method: endpoint.method,
|
||||
query,
|
||||
raw: true,
|
||||
});
|
||||
|
||||
@ -20,6 +20,7 @@ const pageSize = 50;
|
||||
const ledgerTypeOptions = [
|
||||
{ label: "后台入账", value: "admin_stock_credit" },
|
||||
{ label: "币商转用户", value: "seller_transfer" },
|
||||
{ label: "向子币商转账", value: "sub_seller_transfer" },
|
||||
{ label: "工资转币商", value: "salary_transfer_received" },
|
||||
];
|
||||
|
||||
@ -27,12 +28,14 @@ const ledgerTypeLabels = {
|
||||
admin_stock_credit: "后台入账",
|
||||
salary_transfer_received: "工资转币商",
|
||||
seller_transfer: "币商转用户",
|
||||
sub_seller_transfer: "向子币商转账",
|
||||
};
|
||||
|
||||
const bizTypeLabels = {
|
||||
coin_seller_coin_compensation: "金币补偿",
|
||||
coin_seller_stock_deduction: "USDT扣除",
|
||||
coin_seller_stock_purchase: "USDT进货",
|
||||
coin_seller_sub_transfer: "向子币商转账",
|
||||
coin_seller_transfer: "币商转用户",
|
||||
manual_credit: "金币增加",
|
||||
salary_transfer_to_coin_seller: "工资转币商",
|
||||
|
||||
@ -0,0 +1,59 @@
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import { afterEach, expect, test, vi } from "vitest";
|
||||
import { CoinSellerLedgerTable } from "@/features/operations/components/CoinSellerLedgerTable.jsx";
|
||||
import { usePaginatedQuery } from "@/shared/hooks/usePaginatedQuery.js";
|
||||
|
||||
vi.mock("@/shared/hooks/usePaginatedQuery.js", () => ({
|
||||
usePaginatedQuery: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/shared/ui/ToastProvider.jsx", () => ({
|
||||
useToast: () => ({ showToast: vi.fn() }),
|
||||
}));
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test("coin seller ledger shows sub seller transfer type and filter option", () => {
|
||||
vi.mocked(usePaginatedQuery).mockReturnValue(queryFixture([subSellerTransferEntry()]));
|
||||
|
||||
render(<CoinSellerLedgerTable />);
|
||||
|
||||
expect(screen.getByText("向子币商转账")).toBeInTheDocument();
|
||||
expect(screen.getByText("Parent seller")).toBeInTheDocument();
|
||||
expect(screen.getByText("Child seller")).toBeInTheDocument();
|
||||
expect(screen.getByText("-80")).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getAllByRole("button", { name: /流水类型/ })[0]);
|
||||
|
||||
expect(screen.getAllByText("向子币商转账").length).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
|
||||
function queryFixture(items) {
|
||||
return {
|
||||
data: { items, page: 1, pageSize: 50, total: items.length },
|
||||
error: null,
|
||||
loading: false,
|
||||
reload: vi.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
function subSellerTransferEntry() {
|
||||
return {
|
||||
amount: 80,
|
||||
availableDelta: -80,
|
||||
bizType: "coin_seller_sub_transfer",
|
||||
commandId: "cmd-sub-transfer",
|
||||
counterpartyUserId: "5001",
|
||||
createdAtMs: 1760000000000,
|
||||
direction: "expense",
|
||||
entryId: 991,
|
||||
ledgerType: "sub_seller_transfer",
|
||||
metadata: { child_user_id: 5001, parent_user_id: 3001 },
|
||||
receiver: { displayUserId: "5001", userId: "5001", username: "Child seller" },
|
||||
seller: { displayUserId: "3001", userId: "3001", username: "Parent seller" },
|
||||
sellerBalanceAfter: 920,
|
||||
transactionId: "tx-sub-transfer",
|
||||
};
|
||||
}
|
||||
@ -110,6 +110,7 @@ export const API_OPERATIONS = {
|
||||
enableResourceGroup: "enableResourceGroup",
|
||||
enableResourceShopItem: "enableResourceShopItem",
|
||||
expireRoomRpsChallenge: "expireRoomRpsChallenge",
|
||||
exportCoinSellerLedger: "exportCoinSellerLedger",
|
||||
exportLoginLogs: "exportLoginLogs",
|
||||
exportOperationLogs: "exportOperationLogs",
|
||||
exportRechargeBills: "exportRechargeBills",
|
||||
@ -150,6 +151,7 @@ export const API_OPERATIONS = {
|
||||
getSocialBiKpi: "getSocialBiKpi",
|
||||
getSocialBiMaster: "getSocialBiMaster",
|
||||
getSocialBiOverview: "getSocialBiOverview",
|
||||
getSocialBiRequirements: "getSocialBiRequirements",
|
||||
getTemporaryPaymentLink: "getTemporaryPaymentLink",
|
||||
getUser: "getUser",
|
||||
getWeeklyStarCycle: "getWeeklyStarCycle",
|
||||
@ -1037,6 +1039,13 @@ export const API_ENDPOINTS: Record<ApiOperationId, ApiEndpoint> = {
|
||||
permission: "game:update",
|
||||
permissions: ["game:update"]
|
||||
},
|
||||
exportCoinSellerLedger: {
|
||||
method: "GET",
|
||||
operationId: API_OPERATIONS.exportCoinSellerLedger,
|
||||
path: "/v1/admin/operations/coin-seller-ledger/export",
|
||||
permission: "coin-seller-ledger:view",
|
||||
permissions: ["coin-seller-ledger:view"]
|
||||
},
|
||||
exportLoginLogs: {
|
||||
method: "GET",
|
||||
operationId: API_OPERATIONS.exportLoginLogs,
|
||||
@ -1317,6 +1326,13 @@ export const API_ENDPOINTS: Record<ApiOperationId, ApiEndpoint> = {
|
||||
permission: "overview:view",
|
||||
permissions: ["overview:view"]
|
||||
},
|
||||
getSocialBiRequirements: {
|
||||
method: "GET",
|
||||
operationId: API_OPERATIONS.getSocialBiRequirements,
|
||||
path: "/v1/admin/databi/social/requirements",
|
||||
permission: "overview:view",
|
||||
permissions: ["overview:view"]
|
||||
},
|
||||
getTemporaryPaymentLink: {
|
||||
method: "GET",
|
||||
operationId: API_OPERATIONS.getTemporaryPaymentLink,
|
||||
|
||||
237
src/shared/api/generated/schema.d.ts
vendored
237
src/shared/api/generated/schema.d.ts
vendored
@ -1876,6 +1876,22 @@ export interface paths {
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/admin/operations/coin-seller-ledger/export": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get: operations["exportCoinSellerLedger"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/admin/operations/full-server-notices/fanout": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
@ -2148,6 +2164,22 @@ export interface paths {
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/admin/databi/social/requirements": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get: operations["getSocialBiRequirements"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/admin/databi/social/funnel": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
@ -4064,6 +4096,12 @@ export interface components {
|
||||
pageSize: number;
|
||||
total: number;
|
||||
};
|
||||
ApiPageCoinSellerLedger: {
|
||||
items: components["schemas"]["CoinSellerLedger"][];
|
||||
page: number;
|
||||
pageSize: number;
|
||||
total: number;
|
||||
};
|
||||
ApiPageBDProfile: {
|
||||
items: components["schemas"]["BDProfile"][];
|
||||
page: number;
|
||||
@ -4217,6 +4255,9 @@ export interface components {
|
||||
ApiResponseCoinSeller: components["schemas"]["Envelope"] & {
|
||||
data?: components["schemas"]["CoinSeller"];
|
||||
};
|
||||
ApiResponseCoinSellerLedgerPage: components["schemas"]["Envelope"] & {
|
||||
data?: components["schemas"]["ApiPageCoinSellerLedger"];
|
||||
};
|
||||
ApiResponseBDProfilePage: components["schemas"]["Envelope"] & {
|
||||
data?: components["schemas"]["ApiPageBDProfile"];
|
||||
};
|
||||
@ -4244,6 +4285,70 @@ export interface components {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
};
|
||||
ApiResponseSocialBiRequirements: components["schemas"]["Envelope"] & {
|
||||
data?: components["schemas"]["SocialBiRequirementsResult"];
|
||||
};
|
||||
SocialBiRequirementsResult: {
|
||||
apps: components["schemas"]["SocialBiRequirementsApp"][];
|
||||
access?: components["schemas"]["SocialBiRequirementsAccess"];
|
||||
};
|
||||
SocialBiRequirementsApp: {
|
||||
app_code: string;
|
||||
app_name: string;
|
||||
kind: string;
|
||||
restricted?: boolean;
|
||||
allowed_region_ids?: number[];
|
||||
sections: components["schemas"]["SocialBiRequirementsSection"][];
|
||||
/** Format: int64 */
|
||||
updated_at_ms?: number;
|
||||
error?: string;
|
||||
};
|
||||
SocialBiRequirementsSection: {
|
||||
/** @enum {string} */
|
||||
key: "new_users" | "revenue" | "retention" | "active" | "gift" | "game";
|
||||
label?: string;
|
||||
filters?: components["schemas"]["SocialBiRequirementsFilters"];
|
||||
columns: components["schemas"]["SocialBiRequirementsColumn"][];
|
||||
metric_definitions?: components["schemas"]["SocialBiRequirementsMetricDefinition"][];
|
||||
total?: components["schemas"]["SocialBiRequirementsRow"];
|
||||
daily_series: components["schemas"]["SocialBiRequirementsRow"][];
|
||||
};
|
||||
SocialBiRequirementsFilters: {
|
||||
/** @enum {string} */
|
||||
user_role?: "all" | "host" | "user";
|
||||
/** @enum {string} */
|
||||
payer_type?: "all" | "paid" | "unpaid";
|
||||
};
|
||||
SocialBiRequirementsColumn: {
|
||||
key: string;
|
||||
label: string;
|
||||
/** @description count、ratio、money_minor、coin、duration_ms 或 number。 */
|
||||
type: string;
|
||||
tooltip?: string;
|
||||
};
|
||||
SocialBiRequirementsMetricDefinition: {
|
||||
metric: string;
|
||||
definition: string;
|
||||
};
|
||||
/** @description 日明细或汇总行;指标以 columns.key 对应的动态字段返回,兼容历史 metrics 对象。 */
|
||||
SocialBiRequirementsRow: {
|
||||
stat_day?: string;
|
||||
label?: string;
|
||||
metrics?: {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
} & {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
SocialBiRequirementsAccess: {
|
||||
all?: boolean;
|
||||
scopes?: components["schemas"]["SocialBiRequirementsScope"][];
|
||||
};
|
||||
SocialBiRequirementsScope: {
|
||||
app_code?: string;
|
||||
/** Format: int64 */
|
||||
region_id?: number;
|
||||
};
|
||||
ApiResponseFinanceApplication: components["schemas"]["Envelope"] & {
|
||||
data?: components["schemas"]["FinanceApplication"];
|
||||
};
|
||||
@ -4392,6 +4497,51 @@ export interface components {
|
||||
userId: string;
|
||||
username?: string;
|
||||
};
|
||||
CoinLedgerUser: {
|
||||
avatar?: string;
|
||||
displayUserId?: string;
|
||||
userId: string;
|
||||
username?: string;
|
||||
};
|
||||
CoinSellerLedger: {
|
||||
/** Format: int64 */
|
||||
amount: number;
|
||||
/** Format: int64 */
|
||||
availableDelta: number;
|
||||
/** @enum {string} */
|
||||
bizType: "coin_seller_stock_purchase" | "coin_seller_stock_deduction" | "coin_seller_recharge" | "coin_seller_coin_compensation" | "coin_seller_transfer" | "coin_seller_sub_transfer" | "salary_transfer_to_coin_seller" | "manual_credit";
|
||||
commandId?: string;
|
||||
counterpartyUserId?: string;
|
||||
/** Format: int64 */
|
||||
createdAtMs: number;
|
||||
/** @enum {string} */
|
||||
direction: "income" | "expense";
|
||||
/** Format: int64 */
|
||||
entryId: number;
|
||||
/** @enum {string} */
|
||||
ledgerType: "admin_stock_credit" | "seller_transfer" | "sub_seller_transfer" | "salary_transfer_received";
|
||||
metadata?: {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
operator?: {
|
||||
adminId?: string;
|
||||
name?: string;
|
||||
username?: string;
|
||||
};
|
||||
operatorUserId?: string;
|
||||
/** Format: int64 */
|
||||
paidAmountMicro?: number;
|
||||
paidCurrencyCode?: string;
|
||||
reason?: string;
|
||||
receiver: components["schemas"]["CoinLedgerUser"];
|
||||
seller: components["schemas"]["CoinLedgerUser"];
|
||||
/** Format: int64 */
|
||||
sellerBalanceAfter: number;
|
||||
stockType?: string;
|
||||
transactionId: string;
|
||||
/** Format: int64 */
|
||||
transferUsdMinor?: number;
|
||||
};
|
||||
CreateCoinSellerPayload: {
|
||||
canManageSubCoinSellers?: boolean;
|
||||
commandId: string;
|
||||
@ -4871,6 +5021,15 @@ export interface components {
|
||||
};
|
||||
};
|
||||
/** @description OK */
|
||||
CoinSellerLedgerPageResponse: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["ApiResponseCoinSellerLedgerPage"];
|
||||
};
|
||||
};
|
||||
/** @description OK */
|
||||
BDProfilePageResponse: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
@ -5150,6 +5309,15 @@ export interface components {
|
||||
};
|
||||
};
|
||||
/** @description OK */
|
||||
SocialBiRequirementsResponse: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["ApiResponseSocialBiRequirements"];
|
||||
};
|
||||
};
|
||||
/** @description OK */
|
||||
TeamListResponse: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
@ -7594,14 +7762,56 @@ export interface operations {
|
||||
};
|
||||
listCoinSellerLedger: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
query?: {
|
||||
page?: components["parameters"]["Page"];
|
||||
page_size?: components["parameters"]["PageSize"];
|
||||
seller_user_id?: number;
|
||||
seller_keyword?: string;
|
||||
seller_filter_user_id?: string;
|
||||
seller_display_user_id?: string;
|
||||
seller_username?: string;
|
||||
ledger_type?: "admin_stock_credit" | "seller_transfer" | "sub_seller_transfer" | "salary_transfer_received";
|
||||
start_at_ms?: number;
|
||||
end_at_ms?: number;
|
||||
};
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
200: components["responses"]["EmptyResponse"];
|
||||
200: components["responses"]["CoinSellerLedgerPageResponse"];
|
||||
};
|
||||
};
|
||||
exportCoinSellerLedger: {
|
||||
parameters: {
|
||||
query?: {
|
||||
page?: components["parameters"]["Page"];
|
||||
page_size?: components["parameters"]["PageSize"];
|
||||
seller_user_id?: number;
|
||||
seller_keyword?: string;
|
||||
seller_filter_user_id?: string;
|
||||
seller_display_user_id?: string;
|
||||
seller_username?: string;
|
||||
ledger_type?: "admin_stock_credit" | "seller_transfer" | "sub_seller_transfer" | "salary_transfer_received";
|
||||
start_at_ms?: number;
|
||||
end_at_ms?: number;
|
||||
};
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description CSV */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"text/csv": string;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
createFullServerNoticeFanout: {
|
||||
@ -8038,6 +8248,29 @@ export interface operations {
|
||||
200: components["responses"]["StatisticsObjectResponse"];
|
||||
};
|
||||
};
|
||||
getSocialBiRequirements: {
|
||||
parameters: {
|
||||
query?: {
|
||||
stat_tz?: string;
|
||||
start_ms?: number;
|
||||
end_ms?: number;
|
||||
app_codes?: string;
|
||||
region_id?: number;
|
||||
/** @description 逗号分隔的大区 ID;用于 Social BI 顶栏多选区域。 */
|
||||
region_ids?: string;
|
||||
section?: "new_users" | "revenue" | "retention" | "active" | "gift" | "game";
|
||||
user_role?: "all" | "host" | "user";
|
||||
payer_type?: "all" | "paid" | "unpaid";
|
||||
};
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
200: components["responses"]["SocialBiRequirementsResponse"];
|
||||
};
|
||||
};
|
||||
getSocialBiFunnel: {
|
||||
parameters: {
|
||||
query?: {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user