yumi aslan数据打通
This commit is contained in:
parent
ea78619f5f
commit
6ed13c690c
@ -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",
|
||||
|
||||
@ -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(<DatabiApp />);
|
||||
|
||||
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() {
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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 ? (
|
||||
<Button
|
||||
className="social-bi-target-button"
|
||||
disableElevation
|
||||
onClick={() => setIsTargetDialogOpen(true)}
|
||||
type="button"
|
||||
variant="outlined"
|
||||
>
|
||||
配置 KPI 目标
|
||||
</Button>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
|
||||
{(activeSection.key === "personnel" || activeSection.key === "department") && master && !canViewAllKpi ? (
|
||||
<p className="social-bi-scope-hint">当前仅展示你负责的 App / 区域数据(我的 KPI)。</p>
|
||||
) : null}
|
||||
|
||||
<SummarySection cards={summaryCards} isLoading={isLoading} />
|
||||
|
||||
<WideDataTable
|
||||
@ -186,6 +208,13 @@ export function SocialBiDashboard() {
|
||||
totalRow={totalRow}
|
||||
/>
|
||||
</section>
|
||||
{isTargetDialogOpen ? (
|
||||
<KpiTargetDialog
|
||||
kpiData={kpiData}
|
||||
onClose={() => setIsTargetDialogOpen(false)}
|
||||
onSaved={handleTargetsSaved}
|
||||
/>
|
||||
) : null}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@ -223,6 +252,7 @@ function SocialBiSidebar({ activeKey, onSectionChange }) {
|
||||
}
|
||||
|
||||
function FilterSection({
|
||||
actions = null,
|
||||
activeSection,
|
||||
filterDefinitions,
|
||||
filterRef,
|
||||
@ -303,6 +333,7 @@ function FilterSection({
|
||||
>
|
||||
{isLoading ? "查询中" : "查询"}
|
||||
</Button>
|
||||
{actions}
|
||||
{loadError ? <span className="social-bi-load-error">{loadError}</span> : null}
|
||||
</section>
|
||||
);
|
||||
@ -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 (
|
||||
<div className="social-bi-dialog-backdrop" onClick={onClose} role="presentation">
|
||||
<section
|
||||
aria-label="配置 KPI 目标"
|
||||
className="social-bi-dialog"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
role="dialog"
|
||||
>
|
||||
<header className="social-bi-dialog-header">
|
||||
<strong>配置充值 KPI 目标</strong>
|
||||
<span>{periodMonth} · 按 运营人员 × App × 区域</span>
|
||||
</header>
|
||||
<div className="social-bi-dialog-body">
|
||||
{drafts.length ? (
|
||||
<table className="social-bi-dialog-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>运营人员</th>
|
||||
<th>App</th>
|
||||
<th>区域</th>
|
||||
<th>当月目标 ($)</th>
|
||||
<th>当日目标 ($)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{drafts.map((draft, index) => (
|
||||
<tr key={`${draft.operatorUserId}-${draft.appCode}-${draft.regionId}`}>
|
||||
<td>{draft.operator}</td>
|
||||
<td>{draft.appName}</td>
|
||||
<td>{draft.regionName}</td>
|
||||
<td>
|
||||
<input
|
||||
inputMode="decimal"
|
||||
onChange={(event) => updateDraft(index, "monthTargetUsd", event.target.value)}
|
||||
type="text"
|
||||
value={draft.monthTargetUsd}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
inputMode="decimal"
|
||||
onChange={(event) => updateDraft(index, "dailyTargetUsd", event.target.value)}
|
||||
type="text"
|
||||
value={draft.dailyTargetUsd}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
) : (
|
||||
<p className="social-bi-dialog-empty">当前没有可配置的负责范围;请先在「用户管理」里为运营人员分配 App/区域数据范围。</p>
|
||||
)}
|
||||
</div>
|
||||
<footer className="social-bi-dialog-footer">
|
||||
{saveError ? <span className="social-bi-load-error">{saveError}</span> : null}
|
||||
<Button disableElevation onClick={onClose} type="button" variant="text">
|
||||
取消
|
||||
</Button>
|
||||
<Button disabled={isSaving || !drafts.length} disableElevation onClick={handleSave} type="button" variant="contained">
|
||||
{isSaving ? "保存中" : "保存目标"}
|
||||
</Button>
|
||||
</footer>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@ -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 };
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -729,6 +729,7 @@ export function FinanceApp() {
|
||||
updateRechargeDetailFilters({ appCode, regionId: "" });
|
||||
setActiveView("rechargeDetails");
|
||||
}}
|
||||
onOpenWithdrawals={() => setActiveView("withdrawals")}
|
||||
/>
|
||||
) : activeView === "recon" && session.canViewAppRechargeDetails ? (
|
||||
<FinanceReconciliation
|
||||
|
||||
@ -20,6 +20,7 @@ export function FinanceOverview({
|
||||
onFiltersChange,
|
||||
onGoUnsynced,
|
||||
onOpenApp,
|
||||
onOpenWithdrawals,
|
||||
overview,
|
||||
perAppSummaries,
|
||||
prevSummary,
|
||||
@ -75,6 +76,12 @@ export function FinanceOverview({
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
<WithdrawalKpi
|
||||
loading={loading}
|
||||
totalUsd={totalUsd}
|
||||
withdrawal={trend.withdrawal}
|
||||
onOpenWithdrawals={onOpenWithdrawals}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="finance-overview__split">
|
||||
@ -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 (
|
||||
<button
|
||||
className="finance-kpi finance-kpi--withdrawal"
|
||||
title="点击进入用户提现申请"
|
||||
type="button"
|
||||
onClick={onOpenWithdrawals}
|
||||
>
|
||||
<span className="finance-kpi__label">用户提现</span>
|
||||
<strong className="finance-kpi__value">
|
||||
{loading
|
||||
? "…"
|
||||
: `USDT ${(data.totalUsdMinor / 100).toLocaleString("zh-CN", { maximumFractionDigits: 2, minimumFractionDigits: 2 })}`}
|
||||
</strong>
|
||||
<small className="finance-kpi__meta">{loading ? "统计中" : metaParts.join(" · ")}</small>
|
||||
<small className="finance-kpi__meta">
|
||||
{loading
|
||||
? ""
|
||||
: `转币商 ${formatUsdMinor(data.transferUsdMinor, "USDT")} · 提现审批 ${formatUsdMinor(data.approvedUsdMinor, "USDT")}`}
|
||||
</small>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function kpiMeta(bucket, key, totalUsd, prevSummary) {
|
||||
const parts = [`${formatAmount(bucket.billCount)} 笔`];
|
||||
if (key !== "total" && totalUsd > 0) {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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<ApiOperationId, ApiEndpoint> = {
|
||||
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<ApiOperationId, ApiEndpoint> = {
|
||||
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<ApiOperationId, ApiEndpoint> = {
|
||||
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,
|
||||
|
||||
157
src/shared/api/generated/schema.d.ts
vendored
157
src/shared/api/generated/schema.d.ts
vendored
@ -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;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user