From 6ed13c690cde35fa003f68562212b150536a89c4 Mon Sep 17 00:00:00 2001 From: zhx Date: Fri, 3 Jul 2026 18:49:51 +0800 Subject: [PATCH] =?UTF-8?q?yumi=20aslan=E6=95=B0=E6=8D=AE=E6=89=93?= =?UTF-8?q?=E9=80=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- contracts/admin-openapi.json | 103 ++++ databi/src/DatabiApp.test.jsx | 156 +++-- databi/src/api.js | 81 ++- databi/src/components/SocialBiDashboard.jsx | 635 +++++++++++++++----- databi/src/data/socialBiDashboard.js | 368 ++---------- databi/src/styles/social-bi.css | 117 ++++ finance/src/FinanceApp.jsx | 1 + finance/src/components/FinanceOverview.jsx | 46 ++ finance/src/format.js | 18 + finance/src/styles/index.css | 6 +- src/app/permissions.ts | 2 + src/shared/api/generated/endpoints.ts | 40 ++ src/shared/api/generated/schema.d.ts | 157 +++++ 13 files changed, 1210 insertions(+), 520 deletions(-) diff --git a/contracts/admin-openapi.json b/contracts/admin-openapi.json index 27bbddd..48cb2b0 100644 --- a/contracts/admin-openapi.json +++ b/contracts/admin-openapi.json @@ -3117,6 +3117,109 @@ "x-permissions": ["payment-product:delete"] } }, + "/admin/databi/social/master": { + "get": { + "operationId": "getSocialBiMaster", + "x-permission": "overview:view", + "responses": { + "200": { + "$ref": "#/components/responses/StatisticsObjectResponse" + } + }, + "x-permissions": ["overview:view"] + } + }, + "/admin/databi/social/overview": { + "get": { + "operationId": "getSocialBiOverview", + "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" } } + ], + "responses": { + "200": { + "$ref": "#/components/responses/StatisticsObjectResponse" + } + }, + "x-permissions": ["overview:view"] + } + }, + "/admin/databi/social/kpi": { + "get": { + "operationId": "getSocialBiKpi", + "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": "period_month", "schema": { "type": "string" } }, + { "in": "query", "name": "app_codes", "schema": { "type": "string" } }, + { "in": "query", "name": "operator_user_id", "schema": { "type": "integer", "format": "int64" } } + ], + "responses": { + "200": { + "$ref": "#/components/responses/StatisticsObjectResponse" + } + }, + "x-permissions": ["overview:view"] + } + }, + "/admin/databi/social/kpi-targets": { + "get": { + "operationId": "listSocialBiKpiTargets", + "x-permission": "overview:view", + "parameters": [ + { "in": "query", "name": "period_month", "schema": { "type": "string" } }, + { "in": "query", "name": "user_id", "schema": { "type": "integer", "format": "int64" } } + ], + "responses": { + "200": { + "$ref": "#/components/responses/StatisticsObjectResponse" + } + }, + "x-permissions": ["overview:view"] + }, + "put": { + "operationId": "replaceSocialBiKpiTargets", + "x-permission": "databi-kpi:manage", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "type": "object", + "properties": { + "userId": { "type": "integer", "format": "int64" }, + "appCode": { "type": "string" }, + "regionId": { "type": "integer", "format": "int64" }, + "periodMonth": { "type": "string" }, + "targetUsdMinor": { "type": "integer", "format": "int64" }, + "dailyTargetUsdMinor": { "type": "integer", "format": "int64" } + } + } + } + } + } + } + } + }, + "responses": { + "200": { + "$ref": "#/components/responses/StatisticsObjectResponse" + } + }, + "x-permissions": ["databi-kpi:manage"] + } + }, "/admin/finance/scope": { "get": { "operationId": "getFinanceScope", diff --git a/databi/src/DatabiApp.test.jsx b/databi/src/DatabiApp.test.jsx index 42ee7e2..d0dbf34 100644 --- a/databi/src/DatabiApp.test.jsx +++ b/databi/src/DatabiApp.test.jsx @@ -1,11 +1,23 @@ import { act, render, screen, within } from "@testing-library/react"; import { afterEach, beforeEach, expect, test, vi } from "vitest"; import { DatabiApp } from "./DatabiApp.jsx"; -import { fetchFilterOptions, fetchSelfGameStatisticsOverview, fetchSocialBiFilterOptions, fetchStatisticsOverview } from "./api.js"; +import { + fetchFilterOptions, + fetchSelfGameStatisticsOverview, + fetchSocialBiKpi, + fetchSocialBiMaster, + fetchSocialBiOverview, + fetchStatisticsOverview +} from "./api.js"; vi.mock("./api.js", () => ({ fetchFilterOptions: vi.fn(), fetchSocialBiFilterOptions: vi.fn(), + fetchSocialBiKpi: vi.fn(), + fetchSocialBiKpiTargets: vi.fn(), + fetchSocialBiMaster: vi.fn(), + fetchSocialBiOverview: vi.fn(), + replaceSocialBiKpiTargets: vi.fn(), fetchSelfGameStatisticsOverview: vi.fn(), fetchStatisticsOverview: vi.fn(), getCurrentAppCode: vi.fn(() => "lalu"), @@ -36,15 +48,15 @@ beforeEach(() => { regionOptions: [{ countryCodes: [], id: "all", label: "全部区域", value: "all" }] }); fetchSelfGameStatisticsOverview.mockResolvedValue({}); - fetchSocialBiFilterOptions.mockResolvedValue({ - appOptions: [ - { label: "全部 App", value: "all" }, - { label: "Lalu", value: "lalu" }, - { label: "Aslan", value: "aslan" }, - { label: "Yumi", value: "yumi" } - ], - operatorOptions: [{ label: "全部人员", searchText: "all 全部人员", value: "all" }] + fetchSocialBiMaster.mockResolvedValue({ + access: { all: true, scopes: [] }, + apps: [], + operators: [], + permissions: { kpi_manage: false, kpi_view_all: true }, + regions: [] }); + fetchSocialBiOverview.mockResolvedValue({ access: { all: true, scopes: [] }, apps: [] }); + fetchSocialBiKpi.mockResolvedValue({ items: [], period_month: "2026-06", summary: {} }); }); afterEach(() => { @@ -212,55 +224,108 @@ test("renders real-room robot gift cards in a separate row", async () => { test("routes databi social page to Social BI and loads real overview rows", async () => { window.history.pushState(null, "", "/databi/social/"); - fetchStatisticsOverview.mockImplementation(async ({ appCode }) => ({ - active_users: appCode === "lalu" ? 20 : 10, - app_name: appCode === "lalu" ? "Lalu" : appCode === "aslan" ? "Aslan" : "Yumi", + const laluTotal = { + active_users: 20, arpu_usd_minor: 410, - arppu_usd_minor: appCode === "aslan" ? null : 615, + arppu_usd_minor: 615, avg_mic_online_ms: 120_000, - coin_seller_recharge_usd_minor: 3400, - coin_seller_stock_coin: 400_000, - coin_seller_transfer_coin: 160_000, - coin_total: appCode === "aslan" ? 5000 : null, consumed_coin: 900_000, consume_output_delta: 450_000, consume_output_ratio: 0.5, - country_breakdown: [], + d1_retention_rate: 0.25, game_profit_rate: 0.17, game_turnover: 300_000, gift_coin_spent: 123_456, google_recharge_usd_minor: 700, - lucky_gift_profit_rate: 0.25, - lucky_gift_turnover: 200_000, - manual_grant_coin: 222, - mic_online_ms: 3_600_000, mifapay_recharge_usd_minor: 800, - new_user_recharge_usd_minor: 1200, - new_users: appCode === "lalu" ? 3 : 1, + new_users: 3, output_coin: 450_000, paid_conversion_rate: 0.2, paid_users: 4, - platform_grant_coin: 111, - recharge_usd_minor: appCode === "lalu" ? 12300 : 5000, - recharge_conversion_rate: 0.2, - recharge_users: 4, - retention: { day1_rate: 0.25, day7_rate: 0.1, day30_rate: 0.05 }, - salary_transfer_coin: 88_000, - salary_usd_minor: appCode === "aslan" ? 2500 : null, - super_lucky_gift_profit_rate: 0.08, - super_lucky_gift_turnover: 50_000 - })); + recharge_usd_minor: 12_300, + recharge_users: 4 + }; + const aslanRegionRow = { + active_users: 10, + new_users: 1, + paid_users: 2, + recharge_usd_minor: 5000, + recharge_users: 2, + region_code: "MENA", + region_id: 11, + region_name: "中东大区" + }; + fetchSocialBiMaster.mockResolvedValue({ + access: { all: true, scopes: [] }, + apps: [ + { app_code: "lalu", app_name: "Lalu", kind: "hyapp" }, + { app_code: "aslan", app_name: "Aslan", kind: "legacy" } + ], + operators: [ + { account: "omar", name: "Omar", scopes: [{ app_code: "aslan", region_id: 11 }], team: "中东运营部", team_id: 2, user_id: 9 } + ], + permissions: { kpi_manage: true, kpi_view_all: true }, + regions: [{ app_code: "aslan", countries: ["SA"], region_code: "MENA", region_id: 11, region_name: "中东大区" }] + }); + fetchSocialBiOverview.mockResolvedValue({ + access: { all: true, scopes: [] }, + apps: [ + { + app_code: "lalu", + app_name: "Lalu", + country_breakdown: [], + daily_country_breakdown: [], + daily_region_breakdown: [], + daily_series: [], + kind: "hyapp", + region_breakdown: [], + total: laluTotal + }, + { + app_code: "aslan", + app_name: "Aslan", + country_breakdown: [], + daily_country_breakdown: [], + daily_region_breakdown: [], + daily_series: [], + kind: "legacy", + region_breakdown: [aslanRegionRow], + total: { active_users: 10, new_users: 1, paid_users: 2, recharge_usd_minor: 5000, recharge_users: 2 } + } + ] + }); + fetchSocialBiKpi.mockResolvedValue({ + items: [ + { + app_code: "aslan", + app_name: "Aslan", + daily_target_usd_minor: 0, + month_attainment_rate: 0.9, + month_recharge_usd_minor: 90_000, + month_target_usd_minor: 100_000, + operator_account: "omar", + operator_name: "Omar", + operator_user_id: 9, + range_new_users: 1, + range_recharge_usd_minor: 5000, + region_id: 11, + region_name: "中东大区", + team: "中东运营部", + team_id: 2 + } + ], + period_month: "2026-06", + summary: { month_recharge_usd_minor: 90_000, month_target_usd_minor: 100_000, operator_count: 1, range_recharge_usd_minor: 5000 } + }); render(); await flushEffects(); expect(screen.getByLabelText("Social BI 数据中心")).toBeTruthy(); - expect(screen.queryByText("App 数据明细")).toBeNull(); - expect(fetchSocialBiFilterOptions).toHaveBeenCalledTimes(1); - expect(fetchStatisticsOverview).toHaveBeenCalledWith(expect.objectContaining({ appCode: "lalu", statTz: "Asia/Shanghai" })); - expect(fetchStatisticsOverview).toHaveBeenCalledWith(expect.objectContaining({ appCode: "aslan", statTz: "Asia/Shanghai" })); - expect(fetchStatisticsOverview).toHaveBeenCalledWith(expect.objectContaining({ appCode: "yumi", statTz: "Asia/Shanghai" })); + expect(fetchSocialBiMaster).toHaveBeenCalledTimes(1); + expect(fetchSocialBiOverview).toHaveBeenCalledWith(expect.objectContaining({ statTz: "Asia/Shanghai" })); + expect(fetchSocialBiKpi).toHaveBeenCalledWith(expect.objectContaining({ statTz: "Asia/Shanghai" })); expect(screen.getAllByText("Lalu").length).toBeGreaterThan(0); expect(screen.getByText("付费用户")).toBeTruthy(); expect(screen.getByText("游戏流水/利润率")).toBeTruthy(); @@ -270,7 +335,18 @@ test("routes databi social page to Social BI and loads real overview rows", asyn expect(screen.getAllByText("300,000").length).toBeGreaterThan(0); expect(screen.getAllByText("17.0%").length).toBeGreaterThan(0); expect(screen.getAllByText("2分").length).toBeGreaterThan(0); - expect(screen.queryByText("$284,300")).toBeNull(); + + await act(async () => { + screen.getByRole("tab", { name: "地区分析" }).click(); + }); + expect(screen.getAllByText("中东大区").length).toBeGreaterThan(0); + + await act(async () => { + screen.getByRole("tab", { name: "人员绩效" }).click(); + }); + expect(screen.getAllByText("Omar").length).toBeGreaterThan(0); + expect(screen.getByRole("button", { name: "配置 KPI 目标" })).toBeTruthy(); + expect(screen.getAllByText(/90.0%|90,000/).length).toBeGreaterThan(0); }); async function flushEffects() { diff --git a/databi/src/api.js b/databi/src/api.js index 67328ce..a933aa6 100644 --- a/databi/src/api.js +++ b/databi/src/api.js @@ -155,11 +155,84 @@ export async function fetchSocialBiFilterOptions({ appCode } = {}) { }; } -async function fetchDatabiData(path, { appCode, query } = {}) { - const response = await fetch(buildURL(path, query), { - credentials: "include", - headers: requestHeaders(appCode) +// 社交 BI 主数据:按当前登录用户的数据范围返回可见 App、区域目录、运营人员及权限开关。 +export async function fetchSocialBiMaster() { + return fetchDatabiData(apiEndpointPath(API_OPERATIONS.getSocialBiMaster)); +} + +export async function fetchSocialBiOverview({ appCodes, endMs, regionId, startMs, statTz }) { + 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); + } + return fetchDatabiData(apiEndpointPath(API_OPERATIONS.getSocialBiOverview), { query }); +} + +export async function fetchSocialBiKpi({ appCodes, endMs, operatorUserId, periodMonth, startMs, statTz }) { + const query = {}; + if (statTz) { + query.stat_tz = statTz; + } + if (startMs) { + query.start_ms = String(startMs); + } + if (endMs) { + query.end_ms = String(endMs); + } + if (periodMonth) { + query.period_month = periodMonth; + } + if (appCodes?.length) { + query.app_codes = appCodes.join(","); + } + if (operatorUserId) { + query.operator_user_id = String(operatorUserId); + } + return fetchDatabiData(apiEndpointPath(API_OPERATIONS.getSocialBiKpi), { query }); +} + +export async function fetchSocialBiKpiTargets({ periodMonth, userId } = {}) { + const query = {}; + if (periodMonth) { + query.period_month = periodMonth; + } + if (userId) { + query.user_id = String(userId); + } + return fetchDatabiData(apiEndpointPath(API_OPERATIONS.listSocialBiKpiTargets), { query }); +} + +export async function replaceSocialBiKpiTargets(items) { + return fetchDatabiData(apiEndpointPath(API_OPERATIONS.replaceSocialBiKpiTargets), { + body: { items }, + method: "PUT" }); +} + +async function fetchDatabiData(path, { appCode, body, method, query } = {}) { + const headers = requestHeaders(appCode); + const init = { + credentials: "include", + headers, + method: method || "GET" + }; + if (body !== undefined) { + headers["Content-Type"] = "application/json"; + init.body = JSON.stringify(body); + } + const response = await fetch(buildURL(path, query), init); const payload = await readJSON(response); if (isAuthExpired(response, payload)) { redirectToLogin(window); diff --git a/databi/src/components/SocialBiDashboard.jsx b/databi/src/components/SocialBiDashboard.jsx index a74d619..ac7ec85 100644 --- a/databi/src/components/SocialBiDashboard.jsx +++ b/databi/src/components/SocialBiDashboard.jsx @@ -7,14 +7,12 @@ import QueryStatsOutlined from "@mui/icons-material/QueryStatsOutlined"; import RepeatOutlined from "@mui/icons-material/RepeatOutlined"; import Button from "@mui/material/Button"; import Checkbox from "@mui/material/Checkbox"; -import { fetchSocialBiFilterOptions, fetchStatisticsOverview } from "../api.js"; +import { fetchSocialBiKpi, fetchSocialBiMaster, fetchSocialBiOverview, replaceSocialBiKpiTargets } from "../api.js"; import { ALL_VALUE, socialBiAppOptions, - socialBiDepartmentOptions, socialBiFilterDefinitions, socialBiOperatorOptions, - socialBiRegionOptions, socialBiSections } from "../data/socialBiDashboard.js"; import { DEFAULT_TIME_ZONE, rangeEndMs, rangeStartMs, thisMonthRange, todayRange } from "../utils/time.js"; @@ -39,17 +37,17 @@ const sectionIcons = { export function SocialBiDashboard() { const [activeSectionKey, setActiveSectionKey] = useState(socialBiSections[0].key); const [filters, setFilters] = useState(() => createInitialFilters()); - const [filterOptions, setFilterOptions] = useState(() => ({ - apps: socialBiAppOptions, - operators: socialBiOperatorOptions - })); + const [master, setMaster] = useState(null); const [loadingToken, setLoadingToken] = useState(0); const [isLoading, setIsLoading] = useState(true); const [loadError, setLoadError] = useState(""); const [realRows, setRealRows] = useState(() => createEmptyRealRows()); + const [kpiData, setKpiData] = useState(null); + const [isTargetDialogOpen, setIsTargetDialogOpen] = useState(false); const [openFilterKey, setOpenFilterKey] = useState(null); const [sortBySection, setSortBySection] = useState(() => createInitialSortState()); const filterSectionRef = useRef(null); + const filterOptions = useMemo(() => filterOptionsFromMaster(master), [master]); const filterDefinitions = useMemo(() => createRuntimeFilterDefinitions(filterOptions), [filterOptions]); const runtimeSections = useMemo(() => applyRealRowsToSections(realRows), [realRows]); const activeSection = useMemo( @@ -61,24 +59,36 @@ export function SocialBiDashboard() { const sortedRows = useMemo(() => sortRows(filteredRows, activeSection.columns, sortState), [activeSection.columns, filteredRows, sortState]); const totalRow = useMemo(() => createTotalRow(activeSection, filteredRows), [activeSection, filteredRows]); const summaryCards = useMemo(() => createSummaryCards(activeSection, filteredRows, totalRow), [activeSection, filteredRows, totalRow]); + const canManageKpi = Boolean(master?.permissions?.kpi_manage); + const canViewAllKpi = Boolean(master?.permissions?.kpi_view_all); useEffect(() => { let ignore = false; const range = dateRangeToCalendarRange(filters.dateRange); const appCodes = selectedSocialBiAppCodes(filters.apps, filterOptions.apps); + const startMs = rangeStartMs(range, SOCIAL_BI_STAT_TZ); + const endMs = rangeEndMs(range, SOCIAL_BI_STAT_TZ); setIsLoading(true); setLoadError(""); - fetchSocialBiOverviewRows({ appCodes, range }) - .then((rows) => { - if (!ignore) { - setRealRows(rows); + Promise.allSettled([ + fetchSocialBiOverview({ appCodes, endMs, startMs, statTz: SOCIAL_BI_STAT_TZ }), + fetchSocialBiKpi({ appCodes, endMs, startMs, statTz: SOCIAL_BI_STAT_TZ }) + ]) + .then(([overviewResult, kpiResult]) => { + if (ignore) { + return; } - }) - .catch((err) => { - if (!ignore) { + const overview = overviewResult.status === "fulfilled" ? overviewResult.value : null; + const kpi = kpiResult.status === "fulfilled" ? kpiResult.value : null; + if (!overview && !kpi) { setRealRows(createEmptyRealRows()); - setLoadError(err.message || "数据加载失败"); + setKpiData(null); + setLoadError(overviewResult.reason?.message || "数据加载失败"); + return; } + setKpiData(kpi); + setRealRows(buildSocialBiRows({ kpi, overview, range })); + setLoadError(collectSocialBiErrors(overview, kpi, overviewResult, kpiResult)); }) .finally(() => { if (!ignore) { @@ -107,25 +117,15 @@ export function SocialBiDashboard() { useEffect(() => { let ignore = false; - fetchSocialBiFilterOptions() - .then((options) => { - if (ignore) { - return; + fetchSocialBiMaster() + .then((data) => { + if (!ignore && data) { + setMaster(data); } - const nextOptions = { - apps: options.appOptions?.length ? options.appOptions : socialBiAppOptions, - operators: options.operatorOptions?.length ? options.operatorOptions : socialBiOperatorOptions - }; - setFilterOptions((current) => { - if (sameOptionValues(current.apps, nextOptions.apps) && sameOptionValues(current.operators, nextOptions.operators)) { - return current; - } - return nextOptions; - }); }) .catch(() => { if (!ignore) { - setFilterOptions({ apps: socialBiAppOptions, operators: socialBiOperatorOptions }); + setMaster(null); } }); return () => { @@ -142,6 +142,11 @@ export function SocialBiDashboard() { setLoadingToken((current) => current + 1); }, []); + const handleTargetsSaved = useCallback(() => { + setIsTargetDialogOpen(false); + setLoadingToken((current) => current + 1); + }, []); + const toggleFilterPopup = useCallback((key) => { setOpenFilterKey((current) => (current === key ? null : key)); }, []); @@ -173,8 +178,25 @@ export function SocialBiDashboard() { onQuery={handleQuery} onToggleDropdown={toggleFilterPopup} openFilterKey={openFilterKey} + actions={ + activeSection.key === "personnel" && canManageKpi ? ( + + ) : null + } /> + {(activeSection.key === "personnel" || activeSection.key === "department") && master && !canViewAllKpi ? ( +

当前仅展示你负责的 App / 区域数据(我的 KPI)。

+ ) : null} + + {isTargetDialogOpen ? ( + setIsTargetDialogOpen(false)} + onSaved={handleTargetsSaved} + /> + ) : null} ); } @@ -223,6 +252,7 @@ function SocialBiSidebar({ activeKey, onSectionChange }) { } function FilterSection({ + actions = null, activeSection, filterDefinitions, filterRef, @@ -303,6 +333,7 @@ function FilterSection({ > {isLoading ? "查询中" : "查询"} + {actions} {loadError ? {loadError} : null} ); @@ -631,61 +662,181 @@ function applyRealRowsToSections(realRows) { })); } -async function fetchSocialBiOverviewRows({ appCodes, range }) { - if (!appCodes.length) { - return createEmptyRealRows(); - } - const startMs = rangeStartMs(range, SOCIAL_BI_STAT_TZ); - const endMs = rangeEndMs(range, SOCIAL_BI_STAT_TZ); - const results = await Promise.allSettled( - appCodes.map(async (appCode) => { - const overview = await fetchStatisticsOverview({ - appCode, - countryId: 0, - endMs, - regionId: "all", - seriesEndMs: endMs, - seriesStartMs: startMs, - startMs, - statTz: SOCIAL_BI_STAT_TZ - }); - return { appCode, overview: overview || {} }; - }) - ); - const fulfilled = results.filter((item) => item.status === "fulfilled").map((item) => item.value); - if (!fulfilled.length && results.length) { - throw results[0].reason || new Error("数据加载失败"); - } +// buildSocialBiRows 把后端 databi 聚合结果映射成五个板块的表格行: +// App 数据/地区/留存来自 overview(服务端已按数据范围裁剪),人员绩效与部门看板来自 KPI 接口。 +function buildSocialBiRows({ kpi, overview, range }) { const rows = createEmptyRealRows(); - fulfilled.forEach(({ appCode, overview }) => { - rows.appData.push(createAppDataRow(appCode, overview, range)); - const countryRows = normalizeSocialCountries(appCode, overview, range); - rows.regional.push(...countryRows.map(createRegionalRow)); - rows.retention.push(...countryRows.map(createRetentionRow)); + (overview?.apps || []).forEach((app) => { + if (app.error) { + return; + } + rows.appData.push(...createAppDailyRows(app, range)); + const regionRows = createRegionRows(app, range); + rows.regional.push(...regionRows.map(createRegionalRow)); + rows.retention.push(...regionRows.map(createRetentionRow)); }); + const kpiRows = createKpiRows(kpi, range); + rows.personnel = kpiRows; + rows.department = createDepartmentRows(kpiRows, range); return rows; } -function createAppDataRow(appCode, overview, range) { - return { - appName: appLabel(appCode, overview), - date: rangeLabel(range), - id: `app-${appCode}`, - ...createSocialMetricFields(overview) - }; +function collectSocialBiErrors(overview, kpi, overviewResult, kpiResult) { + const errors = []; + if (overviewResult?.status === "rejected") { + errors.push(`统计数据加载失败: ${overviewResult.reason?.message || "未知错误"}`); + } + if (kpiResult?.status === "rejected") { + errors.push(`KPI 数据加载失败: ${kpiResult.reason?.message || "未知错误"}`); + } + (overview?.apps || []).forEach((app) => { + if (app.error) { + errors.push(`${app.app_name || app.app_code}: ${app.error}`); + } + }); + Object.entries(kpi?.app_errors || {}).forEach(([appCode, message]) => { + errors.push(`${appCode}: ${message}`); + }); + return errors.join(";"); } -function normalizeSocialCountries(appCode, overview, range) { - const countries = Array.isArray(overview?.country_breakdown) ? overview.country_breakdown : []; - return countries.map((country, index) => ({ - appCode, - appName: appLabel(appCode, overview), - country, - date: rangeLabel(range), - id: `${appCode}-country-${country.country_code || country.countryCode || country.country_id || country.countryId || index}`, - region: countryLabel(country), - ...createSocialMetricFields(country) - })); +function createAppDailyRows(app, range) { + const appName = app.app_name || appCodeLabel(app.app_code); + const series = Array.isArray(app.daily_series) ? app.daily_series : []; + // 单日区间直接用整段合计(保留留存等区间口径字段),多日区间展开成 天×App 行,与截图口径一致。 + if (series.length > 1) { + return series.map((dayRow) => ({ + appCode: app.app_code, + appName, + date: dayRow.stat_day || dayRow.label || rangeLabel(range), + id: `app-${app.app_code}-${dayRow.stat_day || dayRow.label}`, + ...createSocialMetricFields(dayRow) + })); + } + return [ + { + appCode: app.app_code, + appName, + date: rangeLabel(range), + id: `app-${app.app_code}`, + ...createSocialMetricFields(app.total || {}) + } + ]; +} + +function createRegionRows(app, range) { + const appName = app.app_name || appCodeLabel(app.app_code); + const daily = Array.isArray(app.daily_region_breakdown) ? app.daily_region_breakdown : []; + const totals = Array.isArray(app.region_breakdown) ? app.region_breakdown : []; + const useDaily = daily.length > 0 && new Set(daily.map((row) => row.stat_day || row.label)).size > 1; + const source = useDaily ? daily : totals; + return source.map((row, index) => { + const regionId = Number(row.region_id ?? 0); + const date = useDaily ? row.stat_day || row.label || rangeLabel(range) : rangeLabel(range); + return { + appCode: app.app_code, + appName, + date, + id: `${app.app_code}-region-${regionId || index}-${date}`, + region: row.region_name || row.region_code || `区域 ${regionId}`, + regionId, + regionKey: `${app.app_code}:${regionId}`, + ...createSocialMetricFields(row) + }; + }); +} + +function createKpiRows(kpi, range) { + const days = rangeDayCount(range); + return (kpi?.items || []).map((item) => { + const rangeRecharge = moneyMajor(readOptionalNumber(item, "range_recharge_usd_minor")); + const monthRecharge = moneyMajor(readOptionalNumber(item, "month_recharge_usd_minor")); + const monthTarget = moneyMajor(Number(item.month_target_usd_minor || 0)) || 0; + const dailyTarget = moneyMajor(Number(item.daily_target_usd_minor || 0)) || 0; + const dayTarget = dailyTarget > 0 ? dailyTarget * days : monthTarget > 0 ? (monthTarget / 30) * days : 0; + return { + appCode: item.app_code, + appName: item.app_name || appCodeLabel(item.app_code), + dataError: item.data_error || "", + date: rangeLabel(range), + dayProgress: dayTarget > 0 ? { actual: rangeRecharge || 0, target: dayTarget } : { actual: rangeRecharge || 0, noGoal: true, target: 0 }, + id: `kpi-${item.operator_user_id}-${item.app_code}-${item.region_id}`, + monthKpiRate: item.month_attainment_rate === null || item.month_attainment_rate === undefined ? null : Number(item.month_attainment_rate) * 100, + monthProgress: monthTarget > 0 ? { actual: monthRecharge || 0, target: monthTarget } : { actual: monthRecharge || 0, noGoal: true, target: 0 }, + monthRecharge, + monthTarget: monthTarget > 0 ? monthTarget : null, + newUsers: readOptionalNumber(item, "range_new_users"), + operator: item.operator_name || item.operator_account || `#${item.operator_user_id}`, + operatorUserId: item.operator_user_id, + recharge: rangeRecharge, + region: item.region_name || "全部区域", + regionId: Number(item.region_id ?? 0), + regionKey: `${item.app_code}:${Number(item.region_id ?? 0)}`, + team: item.team || "", + teamId: item.team_id ?? null + }; + }); +} + +function createDepartmentRows(kpiRows, range) { + const groups = new Map(); + kpiRows.forEach((row) => { + const team = row.team || "未分组"; + const key = `${team}|${row.appCode}`; + const group = groups.get(key) || { + appCode: row.appCode, + appName: row.appName, + date: rangeLabel(range), + dayActual: 0, + dayTarget: 0, + department: team, + id: `dept-${key}`, + monthActual: 0, + monthRecharge: 0, + monthTarget: 0, + newUsers: null, + recharge: null, + regions: new Set(), + team + }; + group.recharge = sumOptionalNumbers(group.recharge, row.recharge); + group.newUsers = sumOptionalNumbers(group.newUsers, row.newUsers); + group.monthRecharge = sumOptionalNumbers(group.monthRecharge, row.monthRecharge) || 0; + group.monthTarget += row.monthTarget || 0; + group.dayActual += Number(row.dayProgress?.actual || 0); + group.dayTarget += row.dayProgress?.noGoal ? 0 : Number(row.dayProgress?.target || 0); + group.monthActual += Number(row.monthProgress?.actual || 0); + group.regions.add(row.region); + groups.set(key, group); + }); + return [...groups.values()].map((group) => { + const regionNames = [...group.regions]; + return { + appCode: group.appCode, + appName: group.appName, + date: group.date, + dayProgress: group.dayTarget > 0 ? { actual: group.dayActual, target: group.dayTarget } : { actual: group.dayActual, noGoal: true, target: 0 }, + department: group.department, + id: group.id, + monthKpiRate: group.monthTarget > 0 ? (group.monthActual / group.monthTarget) * 100 : null, + monthProgress: group.monthTarget > 0 ? { actual: group.monthActual, target: group.monthTarget } : { actual: group.monthActual, noGoal: true, target: 0 }, + monthRecharge: group.monthRecharge, + monthTarget: group.monthTarget > 0 ? group.monthTarget : null, + newUsers: group.newUsers, + recharge: group.recharge, + region: regionNames.length > 2 ? `${regionNames.slice(0, 2).join(" / ")} 等${regionNames.length}个` : regionNames.join(" / "), + team: group.team + }; + }); +} + +function rangeDayCount(range) { + const start = new Date(`${range.start}T00:00:00Z`).getTime(); + const end = new Date(`${range.end}T00:00:00Z`).getTime(); + if (!Number.isFinite(start) || !Number.isFinite(end) || end < start) { + return 1; + } + return Math.round((end - start) / 86400000) + 1; } function createSocialMetricFields(source) { @@ -737,25 +888,11 @@ function createSocialMetricFields(source) { } function createRegionalRow(row) { - return { - ...row, - agencyCount: null, - bdCount: null, - coinSellerCoins: readOptionalNumber(row.country, "coin_seller_stock_coin", "coinSellerStockCoin"), - coinSellerCount: null, - hostCount: null - }; + return { ...row, id: `regional-${row.id}` }; } function createRetentionRow(row) { - return { - ...row, - agencyActive: null, - bdActive: null, - coinSellerActive: null, - ecosystemActive: null, - hostActive: null - }; + return { ...row, id: `retention-${row.id}` }; } function selectedSocialBiAppCodes(selectedValues, appOptions) { @@ -802,19 +939,11 @@ function rangeLabel(range) { return range?.start === range?.end ? range.start : `${range.start} ~ ${range.end}`; } -function appLabel(appCode, overview) { - return overview?.app_name || overview?.appName || appCodeLabel(appCode); -} - function appCodeLabel(appCode) { const normalized = String(appCode || "").toLowerCase(); return socialBiAppOptions.find((option) => option.value === normalized)?.label || normalized || "-"; } -function countryLabel(country) { - return country?.country || country?.country_name || country?.countryName || country?.country_code || country?.countryCode || "未知地区"; -} - function retentionPercent(source, topLevelKey, retentionKey) { const value = readOptionalNumber(source, topLevelKey, retentionKey) ?? readOptionalNumber(source?.retention, retentionKey, topLevelKey); return value === null ? null : value * 100; @@ -905,72 +1034,158 @@ function defaultSortState(section) { return { direction: "desc", key: revenueColumn?.key || section.columns[0].key }; } +// filterOptionsFromMaster 把 master 主数据(已按登录用户数据范围裁剪)转成筛选器选项: +// App 多选、按 App 分组的区域级联、带负责范围的运营人员、按 team 聚合的部门。 +function filterOptionsFromMaster(master) { + if (!master) { + return { + apps: socialBiAppOptions, + departments: [{ label: "全部部门", value: ALL_VALUE }], + operators: socialBiOperatorOptions, + regions: [] + }; + } + const apps = [ + { label: "全部 App", value: ALL_VALUE }, + ...(master.apps || []).map((app) => ({ label: app.app_name || app.app_code, value: app.app_code })) + ]; + const operators = [ + { label: "全部人员", scopes: [], searchText: "all 全部人员", value: ALL_VALUE }, + ...(master.operators || []).map((operator) => ({ + label: operator.name || operator.account || `#${operator.user_id}`, + scopes: operator.scopes || [], + searchText: [operator.name, operator.account, operator.team].filter(Boolean).join(" ").toLowerCase(), + value: String(operator.user_id) + })) + ]; + const teamNames = [...new Set((master.operators || []).map((operator) => operator.team).filter(Boolean))]; + const departments = [ + { label: "全部部门", value: ALL_VALUE }, + ...teamNames.map((name) => ({ label: name, value: name })), + ...((master.operators || []).some((operator) => !operator.team) ? [{ label: "未分组", value: "未分组" }] : []) + ]; + const regionsByApp = new Map(); + (master.regions || []).forEach((region) => { + const list = regionsByApp.get(region.app_code) || []; + list.push(region); + regionsByApp.set(region.app_code, list); + }); + const regions = (master.apps || []) + .map((app) => ({ + children: (regionsByApp.get(app.app_code) || []).map((region) => ({ + label: region.region_name || region.region_code || `区域 ${region.region_id}`, + value: `${app.app_code}:${region.region_id}` + })), + label: app.app_name || app.app_code, + value: `app:${app.app_code}` + })) + .filter((group) => group.children.length); + return { apps, departments, operators, regions }; +} + function createRuntimeFilterDefinitions(filterOptions) { return { ...socialBiFilterDefinitions, apps: { ...socialBiFilterDefinitions.apps, options: filterOptions.apps }, - operators: { ...socialBiFilterDefinitions.operators, options: filterOptions.operators } + departments: { ...socialBiFilterDefinitions.departments, options: filterOptions.departments }, + operators: { ...socialBiFilterDefinitions.operators, options: filterOptions.operators }, + regions: { ...socialBiFilterDefinitions.regions, options: filterOptions.regions } }; } function applySectionFilters(section, filters, filterDefinitions) { - // 前端布局阶段只做可验证的本地过滤:按当前行里已有的 App、部门、人员、地区字段筛选,不推断后端不存在的维度。 + const operatorScopes = selectedOperatorScopes(filters.operators, filterDefinitions.operators.options); return section.rows.filter((row) => { - if (!matchesFlatSelection(row.appName, filters.apps, filterDefinitions.apps.options)) { + if (!matchesAppSelection(row.appCode, filters.apps)) { return false; } - if (section.filters.includes("departments") && !matchesFlatSelection(row.department, filters.departments, socialBiDepartmentOptions)) { + if (section.filters.includes("departments") && !matchesDepartmentSelection(row, filters.departments)) { return false; } - if (section.filters.includes("operators") && row.operator && !matchesFlatSelection(row.operator, filters.operators, filterDefinitions.operators.options)) { + if (section.filters.includes("operators") && !matchesOperatorSelection(row, filters.operators, operatorScopes)) { return false; } - if (section.filters.includes("regions") && !matchesRegionSelection(row.region, filters.regions)) { + if (section.filters.includes("regions") && !matchesRegionSelection(row, filters.regions)) { return false; } return true; }); } -function matchesFlatSelection(rowLabel, values, options) { - if (!values?.length || values.includes(ALL_VALUE) || !rowLabel) { +function matchesAppSelection(appCode, values) { + if (!values?.length || values.includes(ALL_VALUE) || !appCode) { return true; } - const selectedLabels = options.filter((option) => values.includes(option.value)).map((option) => normalizeLabel(option.label)); - const normalizedRowLabel = normalizeLabel(rowLabel); - return selectedLabels.some((label) => normalizedRowLabel.includes(label) || label.includes(normalizedRowLabel)); + return values.includes(String(appCode).toLowerCase()); } -function matchesRegionSelection(rowLabel, values) { - if (!values?.length || values.includes(ALL_VALUE) || !rowLabel) { +function matchesDepartmentSelection(row, values) { + if (!values?.length || values.includes(ALL_VALUE)) { return true; } - const selectedLabels = selectedRegionLabels(values).map(normalizeLabel); - const normalizedRowLabel = normalizeLabel(rowLabel); - return selectedLabels.some((label) => normalizedRowLabel.includes(label) || label.includes(normalizedRowLabel)); + return values.includes(row.team || row.department || "未分组"); } -function selectedRegionLabels(values) { - return socialBiRegionOptions.flatMap((group) => { - const labels = []; - if (values.includes(group.value)) { - labels.push(group.label, ...group.children.map((child) => child.label)); +function selectedOperatorScopes(values, options) { + if (!values?.length || values.includes(ALL_VALUE)) { + return null; + } + const scopes = []; + options.forEach((option) => { + if (values.includes(option.value)) { + scopes.push(...(option.scopes || [])); } - group.children.forEach((child) => { - if (values.includes(child.value)) { - labels.push(child.label); - } - }); - return labels; }); + return scopes; +} + +// 人员绩效行按 operatorUserId 精确过滤;其它板块按所选人员的负责范围(App×区域)过滤行。 +function matchesOperatorSelection(row, values, operatorScopes) { + if (!values?.length || values.includes(ALL_VALUE)) { + return true; + } + if (row.operatorUserId !== undefined) { + return values.includes(String(row.operatorUserId)); + } + if (!operatorScopes) { + return true; + } + if (!operatorScopes.length) { + return false; + } + const appCode = String(row.appCode || "").toLowerCase(); + return operatorScopes.some((scope) => { + if (String(scope.app_code || "").toLowerCase() !== appCode) { + return false; + } + if (row.regionId === undefined || scope.region_id === 0) { + return true; + } + return Number(scope.region_id) === Number(row.regionId); + }); +} + +function matchesRegionSelection(row, values) { + if (!values?.length || values.includes(ALL_VALUE)) { + return true; + } + return values.includes(`app:${row.appCode}`) || values.includes(row.regionKey); } function createSummaryCards(section, rows, totalRow) { - return section.cards.map((card) => ({ - ...card, - delta: null, - value: totalRow[card.key] ?? aggregateLooseValue(rows, card) - })); + return section.cards.map((card) => { + // KPI 达成率是比值,用充值合计/目标合计的加权口径,而不是逐行平均。 + if (card.key === "monthKpiRate") { + const monthRecharge = sumOptionalNumbers(...rows.map((row) => row.monthRecharge)); + const monthTarget = sumOptionalNumbers(...rows.map((row) => row.monthTarget)); + return { ...card, delta: null, value: monthTarget ? ((monthRecharge || 0) / monthTarget) * 100 : null }; + } + return { + ...card, + delta: null, + value: totalRow[card.key] ?? aggregateLooseValue(rows, card) + }; + }); } function createTotalRow(section, rows) { @@ -1113,13 +1328,6 @@ function selectedOptionsLabel(options, values) { return `${selected[0].label} +${selected.length - 1}`; } -function sameOptionValues(left = [], right = []) { - if (left.length !== right.length) { - return false; - } - return left.every((item, index) => item.value === right[index]?.value && item.label === right[index]?.label); -} - function selectedCascadeLabel(options, values) { if (!values?.length || values.includes(ALL_VALUE)) { return "全部地区"; @@ -1159,10 +1367,6 @@ function toggleCascadeChild(currentValues, childValue) { return next.length ? next : [ALL_VALUE]; } -function normalizeLabel(value) { - return String(value || "").trim().toLowerCase(); -} - function formatTypedValue(value, type) { if (value === null || value === undefined || value === "" || (typeof value === "number" && !Number.isFinite(value))) { return "--"; @@ -1228,3 +1432,144 @@ function formatDecimal(value) { } return numeric.toLocaleString("en-US", { maximumFractionDigits: 1, minimumFractionDigits: 1 }); } + +// KpiTargetDialog 按 运营人员×App×区域 编辑当月充值 KPI 目标(美元),保存走 PUT /admin/databi/social/kpi-targets。 +// 月/日目标都填 0 表示清除该条目标。 +function KpiTargetDialog({ kpiData, onClose, onSaved }) { + const periodMonth = kpiData?.period_month || defaultPeriodMonth(); + const [drafts, setDrafts] = useState(() => + (kpiData?.items || []).map((item) => ({ + appCode: item.app_code, + appName: item.app_name || item.app_code, + dailyTargetUsd: minorToUsdInput(item.daily_target_usd_minor), + monthTargetUsd: minorToUsdInput(item.month_target_usd_minor), + operator: item.operator_name || item.operator_account || `#${item.operator_user_id}`, + operatorUserId: item.operator_user_id, + regionId: Number(item.region_id ?? 0), + regionName: item.region_name || "全部区域" + })) + ); + const [isSaving, setIsSaving] = useState(false); + const [saveError, setSaveError] = useState(""); + + const updateDraft = (index, key, value) => { + setDrafts((current) => current.map((draft, draftIndex) => (draftIndex === index ? { ...draft, [key]: value } : draft))); + }; + + const handleSave = async () => { + const items = drafts.map((draft) => ({ + appCode: draft.appCode, + dailyTargetUsdMinor: usdInputToMinor(draft.dailyTargetUsd), + periodMonth, + regionId: draft.regionId, + targetUsdMinor: usdInputToMinor(draft.monthTargetUsd), + userId: draft.operatorUserId + })); + if (items.some((item) => item.targetUsdMinor === null || item.dailyTargetUsdMinor === null)) { + setSaveError("目标金额格式不正确"); + return; + } + setIsSaving(true); + setSaveError(""); + try { + await replaceSocialBiKpiTargets(items); + onSaved(); + } catch (error) { + setSaveError(error.message || "保存失败"); + } finally { + setIsSaving(false); + } + }; + + return ( +
+
event.stopPropagation()} + role="dialog" + > +
+ 配置充值 KPI 目标 + {periodMonth} · 按 运营人员 × App × 区域 +
+
+ {drafts.length ? ( + + + + + + + + + + + + {drafts.map((draft, index) => ( + + + + + + + + ))} + +
运营人员App区域当月目标 ($)当日目标 ($)
{draft.operator}{draft.appName}{draft.regionName} + updateDraft(index, "monthTargetUsd", event.target.value)} + type="text" + value={draft.monthTargetUsd} + /> + + updateDraft(index, "dailyTargetUsd", event.target.value)} + type="text" + value={draft.dailyTargetUsd} + /> +
+ ) : ( +

当前没有可配置的负责范围;请先在「用户管理」里为运营人员分配 App/区域数据范围。

+ )} +
+
+ {saveError ? {saveError} : null} + + +
+
+
+ ); +} + +function defaultPeriodMonth() { + const now = new Date(); + return `${now.getFullYear()}-${pad2(now.getMonth() + 1)}`; +} + +function minorToUsdInput(minor) { + const numeric = Number(minor || 0); + if (!Number.isFinite(numeric) || numeric <= 0) { + return "0"; + } + return String(numeric / 100); +} + +function usdInputToMinor(value) { + const trimmed = String(value ?? "").trim(); + if (trimmed === "") { + return 0; + } + const numeric = Number(trimmed); + if (!Number.isFinite(numeric) || numeric < 0) { + return null; + } + return Math.round(numeric * 100); +} diff --git a/databi/src/data/socialBiDashboard.js b/databi/src/data/socialBiDashboard.js index 28a131e..71cc126 100644 --- a/databi/src/data/socialBiDashboard.js +++ b/databi/src/data/socialBiDashboard.js @@ -1,5 +1,7 @@ export const ALL_VALUE = "all"; +// 以下 options 只是接口不可用时的兜底;真实的 App/人员/部门/地区目录来自 +// GET /admin/databi/social/master(按登录用户数据范围裁剪)。 export const socialBiAppOptions = [ { label: "全部 App", value: ALL_VALUE }, { label: "Lalu", value: "lalu" }, @@ -7,49 +9,11 @@ export const socialBiAppOptions = [ { label: "Yumi", value: "yumi" } ]; -export const socialBiOperatorOptions = [ - { label: "全部人员", searchText: "all", value: ALL_VALUE }, - { label: "Ava Chen", searchText: "ava chen 陈", value: "ava" }, - { label: "Nora Li", searchText: "nora li 李", value: "nora" }, - { label: "Omar Saleh", searchText: "omar saleh", value: "omar" }, - { label: "Lina Gomez", searchText: "lina gomez", value: "lina" } -]; +export const socialBiOperatorOptions = [{ label: "全部人员", searchText: "all", value: ALL_VALUE }]; -export const socialBiDepartmentOptions = [ - { label: "全部部门", value: ALL_VALUE }, - { label: "拉美运营一部", value: "latam-ops-1" }, - { label: "中东运营部", value: "mena-ops" }, - { label: "泛娱乐增长组", value: "growth" } -]; +export const socialBiDepartmentOptions = [{ label: "全部部门", value: ALL_VALUE }]; -export const socialBiRegionOptions = [ - { - children: [ - { label: "巴西", value: "br" }, - { label: "墨西哥", value: "mx" }, - { label: "哥伦比亚", value: "co" } - ], - label: "拉美大区", - value: "latam" - }, - { - children: [ - { label: "沙特", value: "sa" }, - { label: "阿联酋", value: "ae" }, - { label: "埃及", value: "eg" } - ], - label: "中东大区", - value: "mena" - }, - { - children: [ - { label: "印度尼西亚", value: "id" }, - { label: "菲律宾", value: "ph" } - ], - label: "东南亚大区", - value: "sea" - } -]; +export const socialBiRegionOptions = []; export const socialBiFilterDefinitions = { apps: { label: "App", options: socialBiAppOptions, type: "multi" }, @@ -62,10 +26,10 @@ export const socialBiFilterDefinitions = { export const socialBiSections = [ { cards: [ - { delta: 8.4, key: "recharge", label: "总充值 ($)", type: "money" }, - { delta: -2.6, key: "newUsers", label: "总新增人数", type: "number" }, - { delta: 3.8, key: "dau", label: "总日活 (DAU)", type: "number" }, - { delta: 1.7, key: "arppu", label: "综合 ARPPU ($)", type: "money" } + { key: "recharge", label: "总充值 ($)", type: "money" }, + { key: "newUsers", label: "总新增人数", type: "number" }, + { key: "dau", label: "总日活 (DAU)", type: "number" }, + { key: "arppu", label: "综合 ARPPU ($)", type: "money" } ], columns: [ textColumn("date", "日期"), @@ -107,61 +71,15 @@ export const socialBiSections = [ filters: ["dateRange", "apps", "operators"], key: "appData", label: "App 数据", - rows: [ - { - appName: "Lalu", - arppu: 42.7, - coins: 49120000, - date: "2026-06-30", - dau: 18520, - id: "app-lalu", - newUsers: 843, - recharge: 284300, - retentionD1: 31.6, - retentionD7: 18.4, - retentionD30: 9.8, - salary: 62310, - turnover: 493820 - }, - { - appName: "Aslan", - arppu: 39.4, - coins: 32760000, - date: "2026-06-30", - dau: 12740, - id: "app-aslan", - newUsers: 612, - recharge: 196740, - retentionD1: 29.2, - retentionD7: 16.1, - retentionD30: 8.7, - salary: 42180, - turnover: 338560 - }, - { - appName: "Yumi", - arppu: 36.2, - coins: 24890000, - date: "2026-06-30", - dau: 9240, - id: "app-yumi", - newUsers: 438, - recharge: 142860, - retentionD1: 27.9, - retentionD7: 15.5, - retentionD30: 7.9, - salary: 31860, - turnover: 251420 - } - ], + rows: [], title: "App 数据" }, { cards: [ - { delta: 7.3, key: "recharge", label: "部门总充值 ($)", type: "money" }, - { delta: 4.8, key: "turnover", label: "部门总流水 ($)", type: "money" }, - { delta: -1.8, key: "newUsers", label: "部门总新增", type: "number" }, - { delta: 2.9, key: "dau", label: "部门总日活 (DAU)", type: "number" } + { key: "recharge", label: "部门区间充值 ($)", type: "money" }, + { key: "monthRecharge", label: "部门当月充值 ($)", type: "money" }, + { key: "newUsers", label: "部门区间新增", type: "number" }, + { key: "monthKpiRate", label: "当月 KPI 达成率 (%)", type: "percent" } ], columns: [ textColumn("date", "日期"), @@ -176,55 +94,15 @@ export const socialBiSections = [ filters: ["dateRange", "apps", "departments"], key: "department", label: "部门看板", - rows: [ - { - appName: "Lalu", - date: "2026-06-30", - dau: 14220, - dayProgress: progress(126800, 140000), - department: "拉美运营一部", - id: "dept-latam", - monthProgress: progress(3020000, 3600000), - newUsers: 516, - recharge: 126800, - region: "拉美大区", - turnover: 225400 - }, - { - appName: "Aslan", - date: "2026-06-30", - dau: 10540, - dayProgress: progress(96800, 90000), - department: "中东运营部", - id: "dept-mena", - monthProgress: progress(2416000, 2700000), - newUsers: 452, - recharge: 96800, - region: "中东大区", - turnover: 164300 - }, - { - appName: "Yumi", - date: "2026-06-30", - dau: 8160, - dayProgress: noGoal(), - department: "泛娱乐增长组", - id: "dept-growth", - monthProgress: progress(1682000, 2100000), - newUsers: 372, - recharge: 74200, - region: "东南亚大区", - turnover: 118900 - } - ], + rows: [], title: "部门看板" }, { cards: [ - { delta: 6.5, key: "recharge", label: "人员充值合计 ($)", type: "money" }, - { delta: 4.4, key: "turnover", label: "人员流水合计 ($)", type: "money" }, - { delta: 2.1, key: "newUsers", label: "新增人数", type: "number" }, - { delta: -3.2, key: "monthKpiRate", label: "当月充值 KPI 达成率 (%)", type: "percent" } + { key: "recharge", label: "人员区间充值合计 ($)", type: "money" }, + { key: "monthRecharge", label: "人员当月充值合计 ($)", type: "money" }, + { key: "monthTarget", label: "当月目标合计 ($)", type: "money" }, + { key: "monthKpiRate", label: "当月充值 KPI 达成率 (%)", type: "percent" } ], columns: [ textColumn("date", "日期"), @@ -239,54 +117,14 @@ export const socialBiSections = [ filters: ["dateRange", "apps", "operators"], key: "personnel", label: "人员绩效", - rows: [ - { - appName: "Lalu", - date: "2026-06-30", - dayProgress: progress(52800, 50000), - id: "person-ava", - monthKpiRate: 88.4, - monthProgress: progress(1182000, 1350000), - newUsers: 211, - operator: "Ava Chen", - recharge: 52800, - region: "巴西 / 墨西哥", - turnover: 94100 - }, - { - appName: "Aslan", - date: "2026-06-30", - dayProgress: progress(41600, 45000), - id: "person-omar", - monthKpiRate: 82.1, - monthProgress: progress(906000, 1100000), - newUsers: 186, - operator: "Omar Saleh", - recharge: 41600, - region: "沙特 / 阿联酋", - turnover: 70400 - }, - { - appName: "Yumi", - date: "2026-06-30", - dayProgress: noGoal(), - id: "person-lina", - monthKpiRate: 75.6, - monthProgress: progress(642000, 850000), - newUsers: 154, - operator: "Lina Gomez", - recharge: 32600, - region: "菲律宾", - turnover: 52900 - } - ], + rows: [], title: "人员绩效" }, { cards: [ - { delta: 9.2, key: "recharge", label: "区域总充值 ($)", type: "money" }, - { delta: -1.1, key: "newUsers", label: "区域总新增", type: "number" }, - { delta: 2.4, key: "arppu", label: "区域综合 ARPPU ($)", type: "money" } + { key: "recharge", label: "区域总充值 ($)", type: "money" }, + { key: "newUsers", label: "区域总新增", type: "number" }, + { key: "arppu", label: "区域综合 ARPPU ($)", type: "money" } ], columns: [ textColumn("date", "日期"), @@ -301,89 +139,25 @@ export const socialBiSections = [ percentColumn("retentionD7", "7日留"), percentColumn("retentionD30", "30日留"), moneyColumn("salary", "存量工资 ($)"), - numberColumn("coins", "金币数量"), - ratioColumn("agencyCount", "Agency数量"), - ratioColumn("hostCount", "Host数量"), - ratioColumn("bdCount", "BD数量"), - ratioColumn("coinSellerCount", "币商数量"), - numberColumn("coinSellerCoins", "币商金币数") + moneyColumn("googleRecharge", "谷歌充值"), + moneyColumn("thirdPartyRecharge", "三方充值"), + moneyColumn("newUserRecharge", "新用户充值"), + numberColumn("giftCoinSpent", "礼物流水"), + flowRateColumn("luckyGiftFlowProfit", "幸运礼物流水/利润率"), + numberColumn("coinSellerTransferCoin", "币商出货金币") ], filters: ["dateRange", "apps", "regions"], key: "regional", label: "地区分析", - rows: [ - { - agencyCount: ratioCount(84, 112), - appName: "Lalu", - arppu: 44.1, - bdCount: ratioCount(26, 34), - coinSellerCoins: 9840000, - coinSellerCount: ratioCount(18, 24), - coins: 36120000, - date: "2026-06-30", - dau: 13280, - hostCount: ratioCount(412, 520), - id: "region-br", - newUsers: 506, - recharge: 206400, - region: "巴西", - retentionD1: 32.8, - retentionD7: 19.4, - retentionD30: 10.2, - salary: 52200, - turnover: 354200 - }, - { - agencyCount: ratioCount(66, 95), - appName: "Aslan", - arppu: 41.5, - bdCount: ratioCount(19, 28), - coinSellerCoins: 7420000, - coinSellerCount: ratioCount(15, 21), - coins: 28450000, - date: "2026-06-30", - dau: 11140, - hostCount: ratioCount(356, 480), - id: "region-sa", - newUsers: 442, - recharge: 173200, - region: "沙特", - retentionD1: 30.4, - retentionD7: 17.7, - retentionD30: 9.1, - salary: 44100, - turnover: 292600 - }, - { - agencyCount: ratioCount(41, 68), - appName: "Yumi", - arppu: 34.8, - bdCount: ratioCount(14, 22), - coinSellerCoins: 4180000, - coinSellerCount: ratioCount(9, 17), - coins: 17260000, - date: "2026-06-30", - dau: 6840, - hostCount: ratioCount(228, 346), - id: "region-ph", - newUsers: 318, - recharge: 94800, - region: "菲律宾", - retentionD1: 27.5, - retentionD7: 14.9, - retentionD30: 7.3, - salary: 22600, - turnover: 164900 - } - ], + rows: [], title: "地区分析" }, { cards: [ - { delta: -1.7, key: "newUsers", label: "总新增", type: "number" }, - { delta: 3.5, key: "dau", label: "总日活", type: "number" }, - { delta: 1.2, key: "retentionD1", label: "次日留存均值 (%)", type: "percent" }, - { delta: 4.1, key: "ecosystemActive", label: "生态角色总活跃数", type: "number" } + { key: "newUsers", label: "总新增", type: "number" }, + { key: "dau", label: "总日活", type: "number" }, + { key: "retentionD1", label: "次日留存均值 (%)", type: "percent" }, + { key: "retentionD7", label: "7日留存均值 (%)", type: "percent" } ], columns: [ textColumn("date", "日期"), @@ -394,64 +168,14 @@ export const socialBiSections = [ percentColumn("retentionD1", "次日留存"), percentColumn("retentionD7", "7日留存"), percentColumn("retentionD30", "30日留存"), - ratioColumn("agencyActive", "Agency活跃数量"), - ratioColumn("hostActive", "Host活跃数量"), - ratioColumn("bdActive", "BD活跃数量"), - ratioColumn("coinSellerActive", "币商活跃数量") + percentColumn("paidConversionRate", "付费转化率"), + moneyColumn("arpu", "ARPU ($)", "average"), + moneyColumn("arppu", "ARPPU ($)", "average") ], filters: ["dateRange", "apps", "regions"], key: "retention", label: "留存分析", - rows: [ - { - agencyActive: ratioCount(84, 112), - appName: "Lalu", - bdActive: ratioCount(26, 34), - coinSellerActive: ratioCount(18, 24), - date: "2026-06-30", - dau: 13280, - ecosystemActive: 540, - hostActive: ratioCount(412, 520), - id: "retain-br", - newUsers: 506, - region: "巴西", - retentionD1: 32.8, - retentionD7: 19.4, - retentionD30: 10.2 - }, - { - agencyActive: ratioCount(66, 95), - appName: "Aslan", - bdActive: ratioCount(19, 28), - coinSellerActive: ratioCount(15, 21), - date: "2026-06-30", - dau: 11140, - ecosystemActive: 456, - hostActive: ratioCount(356, 480), - id: "retain-sa", - newUsers: 442, - region: "沙特", - retentionD1: 30.4, - retentionD7: 17.7, - retentionD30: 9.1 - }, - { - agencyActive: ratioCount(41, 68), - appName: "Yumi", - bdActive: ratioCount(14, 22), - coinSellerActive: ratioCount(9, 17), - date: "2026-06-30", - dau: 6840, - ecosystemActive: 292, - hostActive: ratioCount(228, 346), - id: "retain-ph", - newUsers: 318, - region: "菲律宾", - retentionD1: 27.5, - retentionD7: 14.9, - retentionD30: 7.3 - } - ], + rows: [], title: "留存分析" } ]; @@ -476,10 +200,6 @@ function progressColumn(key, label) { return { aggregate: "progress", align: "left", key, label, type: "progress" }; } -function ratioColumn(key, label) { - return { aggregate: "ratio", align: "right", key, label, type: "ratio" }; -} - function flowRateColumn(key, label) { return { aggregate: "flowRate", align: "right", key, label, type: "flowRate" }; } @@ -491,15 +211,3 @@ function consumeOutputColumn(key, label) { function durationColumn(key, label, aggregate = "sum") { return { aggregate: aggregate === "average" ? "durationAverage" : "duration", align: "right", key, label, type: "duration" }; } - -function progress(actual, target) { - return { actual, target }; -} - -function noGoal() { - return { actual: 0, noGoal: true, target: 0 }; -} - -function ratioCount(active, total) { - return { active, total }; -} diff --git a/databi/src/styles/social-bi.css b/databi/src/styles/social-bi.css index 7d4b767..64540c7 100644 --- a/databi/src/styles/social-bi.css +++ b/databi/src/styles/social-bi.css @@ -654,3 +654,120 @@ transition-duration: 1ms !important; } } + +.social-bi-target-button.MuiButton-root { + height: var(--social-bi-control-height); + min-height: var(--social-bi-control-height); + border-radius: 6px; + border-color: #1688d9; + color: #1688d9; + font-weight: 720; +} + +.social-bi-scope-hint { + margin: 0; + padding: 8px 14px; + border: 1px solid #cfe6f8; + border-radius: 8px; + background: #eef7fe; + color: #0f5d96; + font-size: 13px; + font-weight: 640; +} + +.social-bi-dialog-backdrop { + position: fixed; + inset: 0; + z-index: 60; + display: grid; + place-items: center; + padding: 24px; + background: rgba(7, 22, 38, 0.45); +} + +.social-bi-dialog { + display: flex; + width: min(860px, 100%); + max-height: min(80vh, 720px); + flex-direction: column; + overflow: hidden; + border-radius: 12px; + background: #ffffff; + box-shadow: 0 24px 64px rgba(9, 30, 51, 0.28); +} + +.social-bi-dialog-header { + display: flex; + flex-direction: column; + gap: 4px; + padding: 18px 22px 12px; + border-bottom: 1px solid #e4ecf5; +} + +.social-bi-dialog-header strong { + font-size: 16px; + color: #14263c; +} + +.social-bi-dialog-header span { + color: #5b7089; + font-size: 13px; +} + +.social-bi-dialog-body { + flex: 1; + overflow: auto; + padding: 8px 22px; +} + +.social-bi-dialog-table { + width: 100%; + border-collapse: collapse; + font-size: 13px; +} + +.social-bi-dialog-table th, +.social-bi-dialog-table td { + padding: 9px 10px; + border-bottom: 1px solid #eef3f9; + text-align: left; + white-space: nowrap; +} + +.social-bi-dialog-table th { + position: sticky; + top: 0; + background: #f7fafd; + color: #47607c; + font-weight: 720; +} + +.social-bi-dialog-table input { + width: 120px; + height: 32px; + padding: 0 10px; + border: 1px solid #cddaea; + border-radius: 6px; + font-size: 13px; +} + +.social-bi-dialog-table input:focus { + outline: none; + border-color: #1688d9; + box-shadow: 0 0 0 3px rgba(22, 136, 217, 0.14); +} + +.social-bi-dialog-empty { + margin: 18px 4px; + color: #5b7089; + font-size: 13px; +} + +.social-bi-dialog-footer { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 10px; + padding: 12px 22px 16px; + border-top: 1px solid #e4ecf5; +} diff --git a/finance/src/FinanceApp.jsx b/finance/src/FinanceApp.jsx index b10f6ea..41b7c7d 100644 --- a/finance/src/FinanceApp.jsx +++ b/finance/src/FinanceApp.jsx @@ -729,6 +729,7 @@ export function FinanceApp() { updateRechargeDetailFilters({ appCode, regionId: "" }); setActiveView("rechargeDetails"); }} + onOpenWithdrawals={() => setActiveView("withdrawals")} /> ) : activeView === "recon" && session.canViewAppRechargeDetails ? ( ); })} +
@@ -195,6 +202,45 @@ export function FinanceOverview({ ); } +// WithdrawalKpi 是“用户提现”卡:工资转币商的 USDT + 审核通过的提现申请 USDT;点击进入提现审核页。 +// 提现不是充值渠道,不参与来源筛选联动,占比按“提现 / 充值总和”表达资金回流强度。 +function WithdrawalKpi({ loading, onOpenWithdrawals, totalUsd, withdrawal }) { + const data = withdrawal || { + approvedCount: 0, + approvedUsdMinor: 0, + totalUsdMinor: 0, + transferCount: 0, + transferUsdMinor: 0, + }; + const metaParts = []; + if (totalUsd > 0) { + const share = (data.totalUsdMinor / totalUsd) * 100; + metaParts.push(`占充值 ${share.toLocaleString("zh-CN", { maximumFractionDigits: 1 })}%`); + } + metaParts.push(`${formatAmount(data.transferCount + data.approvedCount)} 笔`); + return ( + + ); +} + function kpiMeta(bucket, key, totalUsd, prevSummary) { const parts = [`${formatAmount(bucket.billCount)} 笔`]; if (key !== "total" && totalUsd > 0) { diff --git a/finance/src/format.js b/finance/src/format.js index 34ee93d..da7a171 100644 --- a/finance/src/format.js +++ b/finance/src/format.js @@ -254,12 +254,20 @@ export const EMPTY_RECHARGE_OVERVIEW = { unsyncedCount: 0, }, regions: [], + withdrawal: { + approvedCount: 0, + approvedUsdMinor: 0, + totalUsdMinor: 0, + transferCount: 0, + transferUsdMinor: 0, + }, }; export function normalizeRechargeOverview(data = {}) { const daily = Array.isArray(data.daily) ? data.daily : []; const regions = Array.isArray(data.regions) ? data.regions : []; const paid = data.googlePaid ?? data.google_paid ?? {}; + const withdrawal = data.withdrawal ?? {}; return { daily: daily.map((item) => ({ coinSellerCoinAmount: numberValue(item.coinSellerCoinAmount ?? item.coin_seller_coin_amount) || 0, @@ -285,6 +293,13 @@ export function normalizeRechargeOverview(data = {}) { regionId: numberOrNull(item.regionId ?? item.region_id), usdMinorAmount: numberValue(item.usdMinorAmount ?? item.usd_minor_amount) || 0, })), + withdrawal: { + approvedCount: numberValue(withdrawal.approvedCount ?? withdrawal.approved_count) || 0, + approvedUsdMinor: numberValue(withdrawal.approvedUsdMinor ?? withdrawal.approved_usd_minor) || 0, + totalUsdMinor: numberValue(withdrawal.totalUsdMinor ?? withdrawal.total_usd_minor) || 0, + transferCount: numberValue(withdrawal.transferCount ?? withdrawal.transfer_count) || 0, + transferUsdMinor: numberValue(withdrawal.transferUsdMinor ?? withdrawal.transfer_usd_minor) || 0, + }, }; } @@ -314,6 +329,9 @@ export function mergeRechargeOverviews(overviews = []) { for (const key of Object.keys(merged.googlePaid)) { merged.googlePaid[key] += overview.googlePaid[key]; } + for (const key of Object.keys(merged.withdrawal)) { + merged.withdrawal[key] += overview.withdrawal[key]; + } } merged.daily = [...dailyByDate.values()].sort((left, right) => left.date.localeCompare(right.date)); return merged; diff --git a/finance/src/styles/index.css b/finance/src/styles/index.css index d3054f6..3f2942f 100644 --- a/finance/src/styles/index.css +++ b/finance/src/styles/index.css @@ -212,7 +212,7 @@ a { .finance-kpi-band { display: grid; - grid-template-columns: repeat(4, minmax(180px, 1fr)); + grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: var(--space-4); } @@ -255,6 +255,10 @@ a { border-left-color: #f9ab00; } +.finance-kpi--withdrawal { + border-left-color: #be185d; +} + .finance-kpi--active { border-color: var(--primary, #4285f4); box-shadow: 0 0 0 2px color-mix(in srgb, var(--primary, #4285f4) 24%, transparent); diff --git a/src/app/permissions.ts b/src/app/permissions.ts index 4f32208..2f176fd 100644 --- a/src/app/permissions.ts +++ b/src/app/permissions.ts @@ -42,6 +42,8 @@ export const PERMISSIONS = { coinSellerStockCredit: "coin-seller:stock-credit", coinSellerExchangeRate: "coin-seller:exchange-rate", hostWithdrawalView: "host-withdrawal:view", + databiKpiManage: "databi-kpi:manage", + databiKpiViewAll: "databi-kpi:view-all", financeView: "finance:view", financeApplicationCreate: "finance-application:create", financeApplicationAudit: "finance-application:audit", diff --git a/src/shared/api/generated/endpoints.ts b/src/shared/api/generated/endpoints.ts index 37f497a..dbc3ddb 100644 --- a/src/shared/api/generated/endpoints.ts +++ b/src/shared/api/generated/endpoints.ts @@ -139,6 +139,9 @@ export const API_OPERATIONS = { getRoomTurnoverRewardConfig: "getRoomTurnoverRewardConfig", getRoomWhitelistConfig: "getRoomWhitelistConfig", getSevenDayCheckInConfig: "getSevenDayCheckInConfig", + getSocialBiKpi: "getSocialBiKpi", + getSocialBiMaster: "getSocialBiMaster", + getSocialBiOverview: "getSocialBiOverview", getTemporaryPaymentLink: "getTemporaryPaymentLink", getUser: "getUser", getWeeklyStarCycle: "getWeeklyStarCycle", @@ -216,6 +219,7 @@ export const API_OPERATIONS = { listRoomTurnoverRewardSettlements: "listRoomTurnoverRewardSettlements", listSelfGames: "listSelfGames", listSevenDayCheckInClaims: "listSevenDayCheckInClaims", + listSocialBiKpiTargets: "listSocialBiKpiTargets", listSplashScreens: "listSplashScreens", listSystemMenus: "listSystemMenus", listTaskDefinitions: "listTaskDefinitions", @@ -248,6 +252,7 @@ export const API_OPERATIONS = { replaceRegionCountries: "replaceRegionCountries", replaceRoleDataScopes: "replaceRoleDataScopes", replaceRolePermissions: "replaceRolePermissions", + replaceSocialBiKpiTargets: "replaceSocialBiKpiTargets", replaceUserFinanceScopes: "replaceUserFinanceScopes", resetUserPassword: "resetUserPassword", retryRedPacketRefund: "retryRedPacketRefund", @@ -1217,6 +1222,27 @@ export const API_ENDPOINTS: Record = { permission: "seven-day-checkin:view", permissions: ["seven-day-checkin:view"] }, + getSocialBiKpi: { + method: "GET", + operationId: API_OPERATIONS.getSocialBiKpi, + path: "/v1/admin/databi/social/kpi", + permission: "overview:view", + permissions: ["overview:view"] + }, + getSocialBiMaster: { + method: "GET", + operationId: API_OPERATIONS.getSocialBiMaster, + path: "/v1/admin/databi/social/master", + permission: "overview:view", + permissions: ["overview:view"] + }, + getSocialBiOverview: { + method: "GET", + operationId: API_OPERATIONS.getSocialBiOverview, + path: "/v1/admin/databi/social/overview", + permission: "overview:view", + permissions: ["overview:view"] + }, getTemporaryPaymentLink: { method: "GET", operationId: API_OPERATIONS.getTemporaryPaymentLink, @@ -1754,6 +1780,13 @@ export const API_ENDPOINTS: Record = { permission: "seven-day-checkin:view", permissions: ["seven-day-checkin:view"] }, + listSocialBiKpiTargets: { + method: "GET", + operationId: API_OPERATIONS.listSocialBiKpiTargets, + path: "/v1/admin/databi/social/kpi-targets", + permission: "overview:view", + permissions: ["overview:view"] + }, listSplashScreens: { method: "GET", operationId: API_OPERATIONS.listSplashScreens, @@ -1968,6 +2001,13 @@ export const API_ENDPOINTS: Record = { permission: "role:permission", permissions: ["role:permission","role:manage"] }, + replaceSocialBiKpiTargets: { + method: "PUT", + operationId: API_OPERATIONS.replaceSocialBiKpiTargets, + path: "/v1/admin/databi/social/kpi-targets", + permission: "databi-kpi:manage", + permissions: ["databi-kpi:manage"] + }, replaceUserFinanceScopes: { method: "PUT", operationId: API_OPERATIONS.replaceUserFinanceScopes, diff --git a/src/shared/api/generated/schema.d.ts b/src/shared/api/generated/schema.d.ts index 0c6f0ac..1dfd3a4 100644 --- a/src/shared/api/generated/schema.d.ts +++ b/src/shared/api/generated/schema.d.ts @@ -2004,6 +2004,70 @@ export interface paths { patch?: never; trace?: never; }; + "/admin/databi/social/master": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["getSocialBiMaster"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/admin/databi/social/overview": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["getSocialBiOverview"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/admin/databi/social/kpi": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["getSocialBiKpi"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/admin/databi/social/kpi-targets": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["listSocialBiKpiTargets"]; + put: operations["replaceSocialBiKpiTargets"]; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/admin/finance/scope": { parameters: { query?: never; @@ -6983,6 +7047,99 @@ export interface operations { 200: components["responses"]["EmptyResponse"]; }; }; + getSocialBiMaster: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: components["responses"]["StatisticsObjectResponse"]; + }; + }; + getSocialBiOverview: { + parameters: { + query?: { + stat_tz?: string; + start_ms?: number; + end_ms?: number; + app_codes?: string; + region_id?: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: components["responses"]["StatisticsObjectResponse"]; + }; + }; + getSocialBiKpi: { + parameters: { + query?: { + stat_tz?: string; + start_ms?: number; + end_ms?: number; + period_month?: string; + app_codes?: string; + operator_user_id?: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: components["responses"]["StatisticsObjectResponse"]; + }; + }; + listSocialBiKpiTargets: { + parameters: { + query?: { + period_month?: string; + user_id?: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: components["responses"]["StatisticsObjectResponse"]; + }; + }; + replaceSocialBiKpiTargets: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + items?: { + /** Format: int64 */ + userId?: number; + appCode?: string; + /** Format: int64 */ + regionId?: number; + periodMonth?: string; + /** Format: int64 */ + targetUsdMinor?: number; + /** Format: int64 */ + dailyTargetUsdMinor?: number; + }[]; + }; + }; + }; + responses: { + 200: components["responses"]["StatisticsObjectResponse"]; + }; + }; getFinanceScope: { parameters: { query?: never;