403 lines
13 KiB
JavaScript
403 lines
13 KiB
JavaScript
export const moneyOperators = [
|
|
{ id: "mina-chen", label: "Mina Chen", department: "平台运营组" },
|
|
{ id: "sarah-ali", label: "Sarah Ali", department: "中东运营组" },
|
|
{ id: "leo-wang", label: "Leo Wang", department: "拉美拓展组" },
|
|
{ id: "diego-costa", label: "Diego Costa", department: "直播增长组" },
|
|
];
|
|
|
|
export const moneyCountries = [
|
|
{ code: "AE", currency: "AED", label: "阿联酋", region: "MENA" },
|
|
{ code: "SA", currency: "SAR", label: "沙特", region: "MENA" },
|
|
{ code: "BR", currency: "BRL", label: "巴西", region: "LATAM" },
|
|
{ code: "MX", currency: "MXN", label: "墨西哥", region: "LATAM" },
|
|
{ code: "ID", currency: "IDR", label: "印尼", region: "SEA" },
|
|
{ code: "TR", currency: "TRY", label: "土耳其", region: "EMEA" },
|
|
];
|
|
|
|
export const moneyAppOptions = [
|
|
["", "全部 App"],
|
|
["hy-live", "HY Live"],
|
|
["voice-party", "Voice Party"],
|
|
["social-pro", "Social Pro"],
|
|
["spark-chat", "Spark Chat"],
|
|
];
|
|
|
|
export const moneyScopeOptions = [
|
|
["", "全部模块"],
|
|
["app", "App 模块"],
|
|
["region", "App 地区模块"],
|
|
];
|
|
|
|
export const moneyProviderOptions = [
|
|
{ code: "mifapay", label: "MiFaPay", countries: ["AE", "SA", "TR"] },
|
|
{ code: "v5pay", label: "V5Pay", countries: ["BR", "MX", "ID"] },
|
|
{ code: "stripe", label: "Stripe", countries: ["AE", "SA", "BR", "MX", "ID", "TR"] },
|
|
];
|
|
|
|
export const trendLabels = ["06/21", "06/22", "06/23", "06/24", "06/25", "06/26", "06/27"];
|
|
|
|
export const moneyModules = [
|
|
{
|
|
id: "hy-live-app",
|
|
appCode: "hy-live",
|
|
appName: "HY Live",
|
|
moduleName: "HY Live 全站",
|
|
scope: "app",
|
|
countryCode: "ALL",
|
|
countryName: "全部国家",
|
|
region: "Global",
|
|
operatorId: "mina-chen",
|
|
operatorName: "Mina Chen",
|
|
department: "平台运营组",
|
|
rechargeUsd: 148420,
|
|
revenueUsd: 316800,
|
|
salaryUsd: 64100,
|
|
paidUsers: 7620,
|
|
newUsers: 42850,
|
|
dau: 960400,
|
|
dayTargetUsd: 150000,
|
|
monthActualUsd: 3410000,
|
|
monthTargetUsd: 3800000,
|
|
retention1: 44.8,
|
|
retention7: 21.4,
|
|
retention30: 9.2,
|
|
linkCount: 46,
|
|
paidLinkCount: 31,
|
|
coinIssued: 15180000,
|
|
rechargeTrend: [112000, 124500, 118900, 133400, 141200, 139800, 148420],
|
|
revenueTrend: [246000, 258400, 249900, 281200, 297800, 301200, 316800],
|
|
},
|
|
{
|
|
id: "voice-party-ksa",
|
|
appCode: "voice-party",
|
|
appName: "Voice Party",
|
|
moduleName: "Voice Party / 沙特",
|
|
scope: "region",
|
|
countryCode: "SA",
|
|
countryName: "沙特",
|
|
region: "MENA-KSA",
|
|
operatorId: "sarah-ali",
|
|
operatorName: "Sarah Ali",
|
|
department: "中东运营组",
|
|
rechargeUsd: 76500,
|
|
revenueUsd: 164200,
|
|
salaryUsd: 29800,
|
|
paidUsers: 3090,
|
|
newUsers: 12800,
|
|
dau: 286400,
|
|
dayTargetUsd: 72000,
|
|
monthActualUsd: 1820000,
|
|
monthTargetUsd: 1750000,
|
|
retention1: 47.5,
|
|
retention7: 24.1,
|
|
retention30: 10.8,
|
|
linkCount: 29,
|
|
paidLinkCount: 22,
|
|
coinIssued: 7820000,
|
|
rechargeTrend: [54800, 61200, 59400, 68000, 70400, 73900, 76500],
|
|
revenueTrend: [126000, 132100, 129600, 145800, 151200, 158000, 164200],
|
|
},
|
|
{
|
|
id: "voice-party-uae",
|
|
appCode: "voice-party",
|
|
appName: "Voice Party",
|
|
moduleName: "Voice Party / 阿联酋",
|
|
scope: "region",
|
|
countryCode: "AE",
|
|
countryName: "阿联酋",
|
|
region: "MENA-UAE",
|
|
operatorId: "sarah-ali",
|
|
operatorName: "Sarah Ali",
|
|
department: "中东运营组",
|
|
rechargeUsd: 43800,
|
|
revenueUsd: 88200,
|
|
salaryUsd: 17200,
|
|
paidUsers: 2020,
|
|
newUsers: 7100,
|
|
dau: 155300,
|
|
dayTargetUsd: 52000,
|
|
monthActualUsd: 930000,
|
|
monthTargetUsd: 1200000,
|
|
retention1: 41.6,
|
|
retention7: 18.7,
|
|
retention30: 7.9,
|
|
linkCount: 18,
|
|
paidLinkCount: 10,
|
|
coinIssued: 3980000,
|
|
rechargeTrend: [39100, 40200, 38800, 42100, 43700, 43000, 43800],
|
|
revenueTrend: [76900, 80200, 78800, 83500, 86100, 87300, 88200],
|
|
},
|
|
{
|
|
id: "social-pro-brazil",
|
|
appCode: "social-pro",
|
|
appName: "Social Pro",
|
|
moduleName: "Social Pro / 巴西",
|
|
scope: "region",
|
|
countryCode: "BR",
|
|
countryName: "巴西",
|
|
region: "LATAM-BR",
|
|
operatorId: "leo-wang",
|
|
operatorName: "Leo Wang",
|
|
department: "拉美拓展组",
|
|
rechargeUsd: 58200,
|
|
revenueUsd: 126300,
|
|
salaryUsd: 22100,
|
|
paidUsers: 4200,
|
|
newUsers: 21100,
|
|
dau: 418700,
|
|
dayTargetUsd: 50000,
|
|
monthActualUsd: 1260000,
|
|
monthTargetUsd: 1080000,
|
|
retention1: 39.2,
|
|
retention7: 16.5,
|
|
retention30: 6.8,
|
|
linkCount: 34,
|
|
paidLinkCount: 26,
|
|
coinIssued: 6200000,
|
|
rechargeTrend: [42100, 43800, 47200, 50100, 52700, 54100, 58200],
|
|
revenueTrend: [89200, 93400, 102200, 109800, 115600, 118500, 126300],
|
|
},
|
|
{
|
|
id: "social-pro-mexico",
|
|
appCode: "social-pro",
|
|
appName: "Social Pro",
|
|
moduleName: "Social Pro / 墨西哥",
|
|
scope: "region",
|
|
countryCode: "MX",
|
|
countryName: "墨西哥",
|
|
region: "LATAM-MX",
|
|
operatorId: "leo-wang",
|
|
operatorName: "Leo Wang",
|
|
department: "拉美拓展组",
|
|
rechargeUsd: 31600,
|
|
revenueUsd: 70600,
|
|
salaryUsd: 14300,
|
|
paidUsers: 2380,
|
|
newUsers: 9400,
|
|
dau: 180200,
|
|
dayTargetUsd: 36000,
|
|
monthActualUsd: 720000,
|
|
monthTargetUsd: 820000,
|
|
retention1: 36.4,
|
|
retention7: 14.8,
|
|
retention30: 5.9,
|
|
linkCount: 15,
|
|
paidLinkCount: 9,
|
|
coinIssued: 3320000,
|
|
rechargeTrend: [27600, 28100, 29400, 30200, 31800, 30500, 31600],
|
|
revenueTrend: [60400, 62100, 64100, 66600, 68900, 68200, 70600],
|
|
},
|
|
{
|
|
id: "spark-chat-indonesia",
|
|
appCode: "spark-chat",
|
|
appName: "Spark Chat",
|
|
moduleName: "Spark Chat / 印尼",
|
|
scope: "region",
|
|
countryCode: "ID",
|
|
countryName: "印尼",
|
|
region: "SEA-ID",
|
|
operatorId: "diego-costa",
|
|
operatorName: "Diego Costa",
|
|
department: "直播增长组",
|
|
rechargeUsd: 39800,
|
|
revenueUsd: 90200,
|
|
salaryUsd: 18600,
|
|
paidUsers: 3510,
|
|
newUsers: 18400,
|
|
dau: 322600,
|
|
dayTargetUsd: 42000,
|
|
monthActualUsd: 860000,
|
|
monthTargetUsd: 940000,
|
|
retention1: 43.1,
|
|
retention7: 19.3,
|
|
retention30: 8.1,
|
|
linkCount: 21,
|
|
paidLinkCount: 14,
|
|
coinIssued: 4460000,
|
|
rechargeTrend: [33800, 35600, 36100, 38200, 39100, 38800, 39800],
|
|
revenueTrend: [76400, 80100, 81200, 84900, 87600, 88400, 90200],
|
|
},
|
|
];
|
|
|
|
export const initialPaymentLinks = [
|
|
createPaymentLinkRecord({
|
|
amountUsd: 299,
|
|
countryCode: "SA",
|
|
moduleId: "voice-party-ksa",
|
|
now: new Date("2026-06-27T08:32:00+08:00"),
|
|
operatorName: "Sarah Ali",
|
|
providerCode: "mifapay",
|
|
status: "paid",
|
|
}),
|
|
createPaymentLinkRecord({
|
|
amountUsd: 129,
|
|
countryCode: "BR",
|
|
moduleId: "social-pro-brazil",
|
|
now: new Date("2026-06-27T10:18:00+08:00"),
|
|
operatorName: "Leo Wang",
|
|
providerCode: "v5pay",
|
|
status: "redirected",
|
|
}),
|
|
createPaymentLinkRecord({
|
|
amountUsd: 499,
|
|
countryCode: "AE",
|
|
moduleId: "voice-party-uae",
|
|
now: new Date("2026-06-27T11:04:00+08:00"),
|
|
operatorName: "Sarah Ali",
|
|
providerCode: "mifapay",
|
|
status: "redirected",
|
|
}),
|
|
];
|
|
|
|
export function filterMoneyModules({
|
|
activeOperatorId = "",
|
|
appCode = "",
|
|
countryCode = "",
|
|
mode = "finance",
|
|
modules = moneyModules,
|
|
operatorKeyword = "",
|
|
scope = "",
|
|
} = {}) {
|
|
const normalizedKeyword = operatorKeyword.trim().toLowerCase();
|
|
|
|
return modules.filter((module) => {
|
|
const matchesRole = mode !== "operator" || module.operatorId === activeOperatorId;
|
|
const matchesApp = !appCode || module.appCode === appCode;
|
|
const matchesScope = !scope || module.scope === scope;
|
|
const matchesCountry =
|
|
!countryCode || module.countryCode === countryCode || (module.scope === "app" && module.countryCode === "ALL");
|
|
const matchesOperator =
|
|
!normalizedKeyword ||
|
|
module.operatorName.toLowerCase().includes(normalizedKeyword) ||
|
|
module.department.toLowerCase().includes(normalizedKeyword);
|
|
|
|
return matchesRole && matchesApp && matchesScope && matchesCountry && matchesOperator;
|
|
});
|
|
}
|
|
|
|
export function summarizeMoneyModules(modules = []) {
|
|
const rechargeUsd = sumBy(modules, "rechargeUsd");
|
|
const revenueUsd = sumBy(modules, "revenueUsd");
|
|
const salaryUsd = sumBy(modules, "salaryUsd");
|
|
const paidUsers = sumBy(modules, "paidUsers");
|
|
const newUsers = sumBy(modules, "newUsers");
|
|
const dau = sumBy(modules, "dau");
|
|
const dayTargetUsd = sumBy(modules, "dayTargetUsd");
|
|
const monthActualUsd = sumBy(modules, "monthActualUsd");
|
|
const monthTargetUsd = sumBy(modules, "monthTargetUsd");
|
|
|
|
return {
|
|
arppuUsd: paidUsers > 0 ? rechargeUsd / paidUsers : 0,
|
|
coinIssued: sumBy(modules, "coinIssued"),
|
|
dau,
|
|
dayProgress: percentage(rechargeUsd, dayTargetUsd),
|
|
dayTargetUsd,
|
|
grossProfitUsd: revenueUsd - salaryUsd,
|
|
linkConversion: percentage(sumBy(modules, "paidLinkCount"), sumBy(modules, "linkCount")),
|
|
monthActualUsd,
|
|
monthProgress: percentage(monthActualUsd, monthTargetUsd),
|
|
monthTargetUsd,
|
|
newUsers,
|
|
paidLinkCount: sumBy(modules, "paidLinkCount"),
|
|
paidUsers,
|
|
rechargeTrend: sumTrend(modules, "rechargeTrend"),
|
|
rechargeUsd,
|
|
retention1: averageBy(modules, "retention1"),
|
|
retention7: averageBy(modules, "retention7"),
|
|
retention30: averageBy(modules, "retention30"),
|
|
revenueTrend: sumTrend(modules, "revenueTrend"),
|
|
revenueUsd,
|
|
salaryUsd,
|
|
totalLinkCount: sumBy(modules, "linkCount"),
|
|
};
|
|
}
|
|
|
|
export function createPaymentLinkRecord({
|
|
amountUsd,
|
|
countryCode,
|
|
moduleId,
|
|
now = new Date(),
|
|
operatorName,
|
|
providerCode,
|
|
status = "redirected",
|
|
}) {
|
|
const module = moneyModules.find((item) => item.id === moduleId) || moneyModules[0];
|
|
const country = moneyCountries.find((item) => item.code === countryCode) || moneyCountries[0];
|
|
const provider = moneyProviderOptions.find((item) => item.code === providerCode) || moneyProviderOptions[0];
|
|
const createdAtMs = now.getTime();
|
|
const suffix = compactTimestamp(now);
|
|
const orderId = `MNY-${country.code}-${suffix}`;
|
|
|
|
return {
|
|
amountUsd: Number(amountUsd) || 0,
|
|
appName: module.appName,
|
|
countryCode: country.code,
|
|
countryName: country.label,
|
|
createdAtMs,
|
|
currency: country.currency,
|
|
id: orderId,
|
|
moduleId: module.id,
|
|
moduleName: module.moduleName,
|
|
operatorName: operatorName || module.operatorName,
|
|
payUrl: `https://pay.hyapp.example/${country.code.toLowerCase()}/${orderId.toLowerCase()}`,
|
|
providerCode: provider.code,
|
|
providerName: provider.label,
|
|
region: country.region,
|
|
status,
|
|
};
|
|
}
|
|
|
|
export function availableCountriesForModule(moduleId) {
|
|
const module = moneyModules.find((item) => item.id === moduleId);
|
|
if (!module || module.scope === "app" || module.countryCode === "ALL") {
|
|
return moneyCountries;
|
|
}
|
|
return moneyCountries.filter((country) => country.code === module.countryCode);
|
|
}
|
|
|
|
export function availableProvidersForCountry(countryCode) {
|
|
return moneyProviderOptions.filter((provider) => provider.countries.includes(countryCode));
|
|
}
|
|
|
|
export function paymentStatusLabel(status) {
|
|
if (status === "paid") {
|
|
return "已支付";
|
|
}
|
|
if (status === "redirected") {
|
|
return "已生成";
|
|
}
|
|
if (status === "failed") {
|
|
return "失败";
|
|
}
|
|
return status || "-";
|
|
}
|
|
|
|
function sumBy(items, key) {
|
|
return items.reduce((total, item) => total + Number(item[key] || 0), 0);
|
|
}
|
|
|
|
function averageBy(items, key) {
|
|
if (!items.length) {
|
|
return 0;
|
|
}
|
|
return sumBy(items, key) / items.length;
|
|
}
|
|
|
|
function sumTrend(items, key) {
|
|
return trendLabels.map((_, index) => items.reduce((total, item) => total + Number(item[key]?.[index] || 0), 0));
|
|
}
|
|
|
|
function percentage(value, total) {
|
|
if (!total) {
|
|
return 0;
|
|
}
|
|
return (Number(value) / Number(total)) * 100;
|
|
}
|
|
|
|
function compactTimestamp(date) {
|
|
const pad = (value) => String(value).padStart(2, "0");
|
|
return `${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}${pad(date.getHours())}${pad(
|
|
date.getMinutes(),
|
|
)}${pad(date.getSeconds())}`;
|
|
}
|