2026-06-28 15:11:48 +08:00

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())}`;
}