From f5d06e1e52aa047b2564bb19218f538d99808f5a Mon Sep 17 00:00:00 2001 From: zhx Date: Tue, 16 Jun 2026 16:59:40 +0800 Subject: [PATCH] =?UTF-8?q?=E7=9B=B8=E5=85=B3=E6=B4=BB=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- contracts/admin-openapi.json | 75 +++ src/app/navigation/menu.js | 2 + src/app/permissions.ts | 7 + src/app/router/routeConfig.ts | 2 + src/features/agency-opening/api.ts | 153 +++++ .../pages/AgencyOpeningPage.jsx | 591 ++++++++++++++++++ src/features/agency-opening/permissions.js | 11 + src/features/agency-opening/routes.js | 12 + src/features/cp-config/api.ts | 42 ++ .../cp-config/hooks/useCPConfigPage.js | 121 +++- src/features/cp-config/pages/CPConfigPage.jsx | 73 ++- src/features/operations/api.test.ts | 52 +- src/features/operations/api.ts | 47 ++ src/features/operations/operations.module.css | 95 +++ .../operations/pages/FullServerNoticePage.jsx | 398 ++++++++++++ src/features/operations/permissions.js | 9 + src/features/operations/routes.js | 8 + src/features/roles/hooks/useRolesPage.js | 1 + src/shared/api/generated/endpoints.ts | 24 + src/shared/api/generated/schema.d.ts | 101 +++ 20 files changed, 1813 insertions(+), 11 deletions(-) create mode 100644 src/features/agency-opening/api.ts create mode 100644 src/features/agency-opening/pages/AgencyOpeningPage.jsx create mode 100644 src/features/agency-opening/permissions.js create mode 100644 src/features/agency-opening/routes.js create mode 100644 src/features/operations/pages/FullServerNoticePage.jsx diff --git a/contracts/admin-openapi.json b/contracts/admin-openapi.json index 1b77c52..95a4824 100644 --- a/contracts/admin-openapi.json +++ b/contracts/admin-openapi.json @@ -132,6 +132,28 @@ "x-permissions": ["user-leaderboard:view"] } }, + "/admin/activity/cp/config": { + "get": { + "operationId": "getCPConfig", + "responses": { + "200": { + "$ref": "#/components/responses/EmptyResponse" + } + }, + "x-permission": "cp-config:view", + "x-permissions": ["cp-config:view"] + }, + "put": { + "operationId": "updateCPConfig", + "responses": { + "200": { + "$ref": "#/components/responses/EmptyResponse" + } + }, + "x-permission": "cp-config:update", + "x-permissions": ["cp-config:update"] + } + }, "/admin/activity/red-packets": { "get": { "operationId": "listRedPackets", @@ -2382,6 +2404,59 @@ "x-permissions": ["coin-seller-ledger:view"] } }, + "/admin/operations/full-server-notices/fanout": { + "post": { + "operationId": "createFullServerNoticeFanout", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": ["message_type", "target_scope", "title", "summary"], + "properties": { + "action_param": { "type": "string" }, + "action_type": { "type": "string" }, + "aggregate_id": { "type": "string" }, + "aggregate_type": { "type": "string" }, + "batch_size": { "type": "integer" }, + "body": { "type": "string" }, + "command_id": { "type": "string" }, + "country": { "type": "string" }, + "expire_at_ms": { "type": "integer", "format": "int64" }, + "icon_url": { "type": "string" }, + "image_url": { "type": "string" }, + "message_type": { "type": "string", "enum": ["system", "activity"] }, + "metadata_json": { "type": "string" }, + "priority": { "type": "integer" }, + "producer_event_type": { "type": "string" }, + "region_id": { "type": "integer", "format": "int64" }, + "sent_at_ms": { "type": "integer", "format": "int64" }, + "summary": { "type": "string" }, + "target_scope": { + "type": "string", + "enum": ["all_active_users", "single_user", "user_ids", "region", "country"] + }, + "target_user_id": { "type": "integer", "format": "int64" }, + "title": { "type": "string" }, + "user_ids": { + "type": "array", + "items": { "type": "integer", "format": "int64" } + } + } + } + } + } + }, + "responses": { + "201": { + "$ref": "#/components/responses/EmptyResponse" + } + }, + "x-permission": "full-server-notice:send", + "x-permissions": ["full-server-notice:send"] + } + }, "/admin/operations/reports": { "get": { "operationId": "listReports", diff --git a/src/app/navigation/menu.js b/src/app/navigation/menu.js index e92b68e..9a0d594 100644 --- a/src/app/navigation/menu.js +++ b/src/app/navigation/menu.js @@ -184,6 +184,7 @@ export const fallbackNavigation = [ routeNavItem("lucky-gift", { icon: RedeemOutlined }), routeNavItem("operation-reports", { icon: FlagOutlined }), routeNavItem("operation-gift-diamond", { icon: DiamondOutlined }), + routeNavItem("operation-full-server-notice", { icon: CampaignOutlined }), ], }, { @@ -212,6 +213,7 @@ export const fallbackNavigation = [ routeNavItem("room-rocket", { icon: RedeemOutlined }), routeNavItem("room-turnover-reward", { icon: PaidOutlined }), routeNavItem("weekly-star", { icon: StarBorderOutlined }), + routeNavItem("agency-opening", { icon: StorefrontOutlined }), routeNavItem("user-leaderboard", { icon: LeaderboardOutlined }), routeNavItem("red-packet", { icon: RedeemOutlined }), routeNavItem("cp-config", { icon: FavoriteBorderOutlined }), diff --git a/src/app/permissions.ts b/src/app/permissions.ts index 9b43b66..4a0bcab 100644 --- a/src/app/permissions.ts +++ b/src/app/permissions.ts @@ -45,6 +45,8 @@ export const PERMISSIONS = { reportView: "report:view", giftDiamondView: "gift-diamond:view", giftDiamondUpdate: "gift-diamond:update", + fullServerNoticeView: "full-server-notice:view", + fullServerNoticeSend: "full-server-notice:send", paymentBillView: "payment-bill:view", paymentProductView: "payment-product:view", paymentProductCreate: "payment-product:create", @@ -159,6 +161,9 @@ export const PERMISSIONS = { weeklyStarCreate: "weekly-star:create", weeklyStarUpdate: "weekly-star:update", weeklyStarSettle: "weekly-star:settle", + agencyOpeningView: "agency-opening:view", + agencyOpeningCreate: "agency-opening:create", + agencyOpeningUpdate: "agency-opening:update", uploadCreate: "upload:create", } as const; @@ -207,6 +212,7 @@ export const MENU_CODES = { operationCoinAdjustment: "operation-coin-adjustment", operationReports: "operation-reports", operationGiftDiamond: "operation-gift-diamond", + operationFullServerNotice: "operation-full-server-notice", luckyGift: "lucky-gift", activities: "activities", dailyTaskList: "daily-task-list", @@ -223,6 +229,7 @@ export const MENU_CODES = { cpConfig: "cp-config", vipConfig: "vip-config", weeklyStar: "weekly-star", + agencyOpening: "agency-opening", geo: "geo", hostOrg: "host-org", hostOrgCountries: "host-org-countries", diff --git a/src/app/router/routeConfig.ts b/src/app/router/routeConfig.ts index c626e99..d182710 100644 --- a/src/app/router/routeConfig.ts +++ b/src/app/router/routeConfig.ts @@ -1,4 +1,5 @@ import { authRoutes } from "@/features/auth/routes.js"; +import { agencyOpeningRoutes } from "@/features/agency-opening/routes.js"; import { achievementRoutes } from "@/features/achievements/routes.js"; import { appConfigRoutes } from "@/features/app-config/routes.js"; import { appUserRoutes } from "@/features/app-users/routes.js"; @@ -52,6 +53,7 @@ export const adminRoutes: AdminRoute[] = [ ...roomRocketRoutes, ...roomTurnoverRewardRoutes, ...weeklyStarRoutes, + ...agencyOpeningRoutes, ...userLeaderboardRoutes, ...redPacketRoutes, ...cpConfigRoutes, diff --git a/src/features/agency-opening/api.ts b/src/features/agency-opening/api.ts new file mode 100644 index 0000000..3aaff2f --- /dev/null +++ b/src/features/agency-opening/api.ts @@ -0,0 +1,153 @@ +import { apiRequest, type QueryParams } from "@/shared/api/request"; + +type Raw = Record; + +export interface AgencyOpeningRewardDto { + rankNo: number; + tierNo: number; + thresholdCoinSpent: number; + rewardCoinAmount: number; +} + +export interface AgencyOpeningCycleDto { + cycleId: string; + activityCode: string; + title: string; + status: string; + startMs: number; + endMs: number; + minHostCount: number; + maxAgencyAgeDays: number; + rewards: AgencyOpeningRewardDto[]; + settledAtMs: number; + createdAtMs: number; + updatedAtMs: number; +} + +export interface AgencyOpeningCyclePayload { + cycle_id?: string; + title: string; + status: string; + start_ms: number; + end_ms: number; + min_host_count: number; + max_agency_age_days: number; + rewards: Array<{ rank_no: number; tier_no?: number; threshold_coin_spent: number; reward_coin_amount: number }>; +} + +export interface AgencyOpeningApplicationDto { + applicationId: string; + cycleId: string; + agencyId: string; + agencyOwnerUserId: string; + agencyName: string; + hostCount: number; + agencyCreatedAtMs: number; + status: string; + scoreCoinAmount: number; + rewardRankNo: number; + rewardThresholdCoinSpent: number; + rewardCoinAmount: number; + walletCommandId: string; + walletTransactionId: string; + failureReason: string; + appliedAtMs: number; + grantedAtMs: number; + updatedAtMs: number; +} + +export function listAgencyOpeningCycles(query: QueryParams = {}) { + return apiRequest<{ items?: Raw[]; total?: number; page?: number; page_size?: number }>( + "/v1/admin/activity/agency-opening/cycles", + { method: "GET", query }, + ).then((page) => ({ ...page, items: (page.items || []).map(normalizeCycle) })); +} + +export function createAgencyOpeningCycle(payload: AgencyOpeningCyclePayload) { + return apiRequest("/v1/admin/activity/agency-opening/cycles", { + body: payload, + method: "POST", + }).then(normalizeCycle); +} + +export function updateAgencyOpeningCycle(cycleId: string, payload: AgencyOpeningCyclePayload) { + return apiRequest( + `/v1/admin/activity/agency-opening/cycles/${encodeURIComponent(cycleId)}`, + { + body: payload, + method: "PUT", + }, + ).then(normalizeCycle); +} + +export function setAgencyOpeningCycleStatus(cycleId: string, status: string) { + return apiRequest( + `/v1/admin/activity/agency-opening/cycles/${encodeURIComponent(cycleId)}/status`, + { body: { status }, method: "PATCH" }, + ).then(normalizeCycle); +} + +export function listAgencyOpeningApplications(query: QueryParams = {}) { + return apiRequest<{ items?: Raw[]; total?: number; page?: number; page_size?: number }>( + "/v1/admin/activity/agency-opening/applications", + { method: "GET", query }, + ).then((page) => ({ ...page, items: (page.items || []).map(normalizeApplication) })); +} + +function normalizeCycle(item: Raw): AgencyOpeningCycleDto { + return { + cycleId: stringValue(item.cycleId ?? item.cycle_id), + activityCode: stringValue(item.activityCode ?? item.activity_code), + title: stringValue(item.title), + status: stringValue(item.status), + startMs: numberValue(item.startMs ?? item.start_ms), + endMs: numberValue(item.endMs ?? item.end_ms), + minHostCount: numberValue(item.minHostCount ?? item.min_host_count), + maxAgencyAgeDays: numberValue(item.maxAgencyAgeDays ?? item.max_agency_age_days), + rewards: Array.isArray(item.rewards) ? item.rewards.map((reward) => normalizeReward(reward as Raw)) : [], + settledAtMs: numberValue(item.settledAtMs ?? item.settled_at_ms), + createdAtMs: numberValue(item.createdAtMs ?? item.created_at_ms), + updatedAtMs: numberValue(item.updatedAtMs ?? item.updated_at_ms), + }; +} + +function normalizeReward(item: Raw): AgencyOpeningRewardDto { + return { + rankNo: numberValue(item.rankNo ?? item.rank_no), + tierNo: numberValue(item.tierNo ?? item.tier_no ?? item.rankNo ?? item.rank_no), + thresholdCoinSpent: numberValue(item.thresholdCoinSpent ?? item.threshold_coin_spent), + rewardCoinAmount: numberValue(item.rewardCoinAmount ?? item.reward_coin_amount), + }; +} + +function normalizeApplication(item: Raw): AgencyOpeningApplicationDto { + return { + applicationId: stringValue(item.applicationId ?? item.application_id), + cycleId: stringValue(item.cycleId ?? item.cycle_id), + agencyId: stringValue(item.agencyId ?? item.agency_id), + agencyOwnerUserId: stringValue(item.agencyOwnerUserId ?? item.agency_owner_user_id), + agencyName: stringValue(item.agencyName ?? item.agency_name), + hostCount: numberValue(item.hostCount ?? item.host_count), + agencyCreatedAtMs: numberValue(item.agencyCreatedAtMs ?? item.agency_created_at_ms), + status: stringValue(item.status), + scoreCoinAmount: numberValue(item.scoreCoinAmount ?? item.score_coin_amount), + rewardRankNo: numberValue(item.rewardRankNo ?? item.reward_rank_no), + rewardThresholdCoinSpent: numberValue(item.rewardThresholdCoinSpent ?? item.reward_threshold_coin_spent), + rewardCoinAmount: numberValue(item.rewardCoinAmount ?? item.reward_coin_amount), + walletCommandId: stringValue(item.walletCommandId ?? item.wallet_command_id), + walletTransactionId: stringValue(item.walletTransactionId ?? item.wallet_transaction_id), + failureReason: stringValue(item.failureReason ?? item.failure_reason), + appliedAtMs: numberValue(item.appliedAtMs ?? item.applied_at_ms), + grantedAtMs: numberValue(item.grantedAtMs ?? item.granted_at_ms), + updatedAtMs: numberValue(item.updatedAtMs ?? item.updated_at_ms), + }; +} + +function numberValue(value: unknown) { + const parsed = Number(value); + return Number.isFinite(parsed) ? parsed : 0; +} + +function stringValue(value: unknown) { + return value === undefined || value === null ? "" : String(value); +} diff --git a/src/features/agency-opening/pages/AgencyOpeningPage.jsx b/src/features/agency-opening/pages/AgencyOpeningPage.jsx new file mode 100644 index 0000000..e4a49db --- /dev/null +++ b/src/features/agency-opening/pages/AgencyOpeningPage.jsx @@ -0,0 +1,591 @@ +import AddOutlined from "@mui/icons-material/AddOutlined"; +import DeleteOutlineOutlined from "@mui/icons-material/DeleteOutlineOutlined"; +import EditOutlined from "@mui/icons-material/EditOutlined"; +import RefreshOutlined from "@mui/icons-material/RefreshOutlined"; +import VisibilityOutlined from "@mui/icons-material/VisibilityOutlined"; +import Box from "@mui/material/Box"; +import Chip from "@mui/material/Chip"; +import Drawer from "@mui/material/Drawer"; +import MenuItem from "@mui/material/MenuItem"; +import Snackbar from "@mui/material/Snackbar"; +import TextField from "@mui/material/TextField"; +import Typography from "@mui/material/Typography"; +import { useEffect, useState } from "react"; +import { + createAgencyOpeningCycle, + listAgencyOpeningApplications, + listAgencyOpeningCycles, + setAgencyOpeningCycleStatus, + updateAgencyOpeningCycle, +} from "@/features/agency-opening/api"; +import { useAgencyOpeningAbilities } from "@/features/agency-opening/permissions.js"; +import { Button } from "@/shared/ui/Button.jsx"; +import { + AdminActionIconButton, + AdminListBody, + AdminListPage, + AdminListToolbar, + AdminRowActions, +} from "@/shared/ui/AdminListLayout.jsx"; +import { DataState } from "@/shared/ui/DataState.jsx"; +import { DataTable } from "@/shared/ui/DataTable.jsx"; +import { TimeText } from "@/shared/ui/TimeText.jsx"; + +const statusOptions = [ + ["draft", "草稿"], + ["active", "启用"], + ["disabled", "停用"], + ["settling", "结算中"], + ["settled", "已结算"], +]; + +const emptyForm = { + cycleId: "", + title: "代理开业活动", + status: "draft", + startMs: "", + endMs: "", + minHostCount: "10", + maxAgencyAgeDays: "30", + rewards: [ + { rankNo: 1, thresholdCoinSpent: "500000", rewardCoinAmount: "50000" }, + { rankNo: 2, thresholdCoinSpent: "1000000", rewardCoinAmount: "100000" }, + { rankNo: 3, thresholdCoinSpent: "3000000", rewardCoinAmount: "300000" }, + { rankNo: 4, thresholdCoinSpent: "5000000", rewardCoinAmount: "500000" }, + { rankNo: 5, thresholdCoinSpent: "10000000", rewardCoinAmount: "500000" }, + ], +}; + +export function AgencyOpeningPage() { + const abilities = useAgencyOpeningAbilities(); + const [cycles, setCycles] = useState([]); + const [applications, setApplications] = useState([]); + const [selectedCycleId, setSelectedCycleId] = useState(""); + const [loading, setLoading] = useState(true); + const [applicationsLoading, setApplicationsLoading] = useState(false); + const [error, setError] = useState(""); + const [drawerOpen, setDrawerOpen] = useState(false); + const [saving, setSaving] = useState(false); + const [form, setForm] = useState(emptyForm); + const [toast, setToast] = useState(""); + + useEffect(() => { + reloadCycles(); + }, []); + + useEffect(() => { + reloadApplications(selectedCycleId); + }, [selectedCycleId]); + + async function reloadCycles() { + setLoading(true); + setError(""); + try { + const result = await listAgencyOpeningCycles({ page_size: 100 }); + const items = result.items || []; + setCycles(items); + setSelectedCycleId((current) => current || items[0]?.cycleId || ""); + } catch (err) { + setError(err instanceof Error ? err.message : "加载失败"); + } finally { + setLoading(false); + } + } + + async function reloadApplications(cycleId) { + setApplicationsLoading(true); + try { + const result = await listAgencyOpeningApplications({ + cycle_id: cycleId || undefined, + page_size: 100, + }); + setApplications(result.items || []); + } catch { + setApplications([]); + } finally { + setApplicationsLoading(false); + } + } + + function openCreate() { + setForm(emptyForm); + setDrawerOpen(true); + } + + function openEdit(cycle) { + setForm({ + cycleId: cycle.cycleId, + title: cycle.title, + status: cycle.status || "draft", + startMs: millisToInput(cycle.startMs), + endMs: millisToInput(cycle.endMs), + minHostCount: String(cycle.minHostCount || 10), + maxAgencyAgeDays: String(cycle.maxAgencyAgeDays || 0), + rewards: (cycle.rewards?.length ? cycle.rewards : emptyForm.rewards).map((reward) => ({ + rankNo: reward.rankNo, + thresholdCoinSpent: String(reward.thresholdCoinSpent || ""), + rewardCoinAmount: String(reward.rewardCoinAmount || ""), + })), + }); + setDrawerOpen(true); + } + + async function submitForm(event) { + event.preventDefault(); + if (!abilities.canCreate && !form.cycleId) return; + if (!abilities.canUpdate && form.cycleId) return; + setSaving(true); + try { + const payload = { + cycle_id: form.cycleId || undefined, + title: form.title.trim(), + status: form.status, + start_ms: inputToMillis(form.startMs), + end_ms: inputToMillis(form.endMs), + min_host_count: Number(form.minHostCount || 0), + max_agency_age_days: Number(form.maxAgencyAgeDays || 0), + rewards: normalizeRewardPayload(form.rewards), + }; + if (form.cycleId) { + await updateAgencyOpeningCycle(form.cycleId, payload); + } else { + await createAgencyOpeningCycle(payload); + } + setToast("已保存代理开业活动周期"); + setDrawerOpen(false); + await reloadCycles(); + } catch (err) { + setToast(err instanceof Error ? err.message : "保存失败"); + } finally { + setSaving(false); + } + } + + async function toggleStatus(cycle) { + if (!abilities.canUpdate) return; + const nextStatus = cycle.status === "active" ? "disabled" : "active"; + try { + await setAgencyOpeningCycleStatus(cycle.cycleId, nextStatus); + setToast(nextStatus === "active" ? "已启用" : "已停用"); + await reloadCycles(); + } catch (err) { + setToast(err instanceof Error ? err.message : "状态更新失败"); + } + } + + function updateReward(index, field, value) { + setForm((current) => ({ + ...current, + rewards: current.rewards.map((reward, itemIndex) => + itemIndex === index ? { ...reward, [field]: value } : reward, + ), + })); + } + + function addRewardTier() { + setForm((current) => ({ + ...current, + rewards: [ + ...current.rewards, + { + rankNo: current.rewards.length + 1, + thresholdCoinSpent: "", + rewardCoinAmount: "", + }, + ], + })); + } + + function removeRewardTier(index) { + setForm((current) => ({ + ...current, + rewards: current.rewards + .filter((_, itemIndex) => itemIndex !== index) + .map((reward, itemIndex) => ({ ...reward, rankNo: itemIndex + 1 })), + })); + } + + return ( + + + + {abilities.canCreate ? ( + + ) : null} + + } + /> + + + ( +
+ {cycle.title || "-"} + {cycle.cycleId || "-"} +
+ ), + }, + { + key: "time", + label: "时间", + width: "minmax(240px, 0.9fr)", + render: (cycle) => ( +
+ + + 至 + +
+ ), + }, + { + key: "policy", + label: "规则", + width: "minmax(200px, 0.8fr)", + render: (cycle) => ( +
+ 主播数 > {cycle.minHostCount} + + 新代理 {cycle.maxAgencyAgeDays || 0} 天内 + +
+ ), + }, + { + key: "rewards", + label: "金币奖励", + width: "minmax(260px, 1fr)", + render: (cycle) => ( +
+ {(cycle.rewards || []).map((reward) => ( + + ))} +
+ ), + }, + { + key: "status", + label: "状态", + width: "112px", + render: (cycle) => ( + + ), + }, + { + key: "actions", + label: "操作", + width: "132px", + fixed: "right", + resizable: false, + render: (cycle) => ( + + } + label="申请记录" + onClick={() => setSelectedCycleId(cycle.cycleId)} + /> + {abilities.canUpdate ? ( + } + label="编辑" + onClick={() => openEdit(cycle)} + /> + ) : null} + {abilities.canUpdate ? ( + + ) : null} + + ), + }, + ]} + emptyLabel="暂无代理开业活动周期" + items={cycles} + minWidth="1220px" + rowKey={(cycle) => cycle.cycleId} + /> +
+
+ + + + 申请记录 + + `${item.agencyName || "-"} / ${item.agencyId || "-"}`, + }, + { + key: "owner", + label: "Owner", + width: "160px", + render: (item) => item.agencyOwnerUserId || "-", + }, + { key: "hosts", label: "主播数", width: "100px", render: (item) => item.hostCount }, + { + key: "score", + label: "开业流水", + width: "120px", + render: (item) => formatNumber(item.scoreCoinAmount), + }, + { + key: "reward", + label: "奖励", + width: "180px", + render: (item) => + item.rewardCoinAmount + ? `流水≥${formatNumber(item.rewardThresholdCoinSpent)} / ${formatNumber(item.rewardCoinAmount)}` + : "-", + }, + { + key: "status", + label: "状态", + width: "112px", + render: (item) => , + }, + { + key: "applied", + label: "申请时间", + width: "180px", + render: (item) => , + }, + { + key: "granted", + label: "发奖时间", + width: "180px", + render: (item) => (item.grantedAtMs ? : "-"), + }, + ]} + emptyLabel={applicationsLoading ? "加载中..." : "暂无申请记录"} + items={applications} + minWidth="1210px" + rowKey={(item) => item.applicationId} + /> + + + setDrawerOpen(false)}> + +
+

+ {form.cycleId ? "编辑代理开业活动" : "新增代理开业活动"} +

+
+
+
+
+ setForm({ ...form, title: event.target.value })} + /> + setForm({ ...form, status: event.target.value })} + > + {statusOptions.slice(0, 3).map(([value, label]) => ( + + {label} + + ))} + + setForm({ ...form, startMs: event.target.value })} + InputLabelProps={{ shrink: true }} + /> + setForm({ ...form, endMs: event.target.value })} + InputLabelProps={{ shrink: true }} + /> + setForm({ ...form, minHostCount: event.target.value })} + /> + setForm({ ...form, maxAgencyAgeDays: event.target.value })} + /> +
+
+
+ + 开业流水档位奖励 + + + + +
+ {form.rewards.map((reward, index) => ( +
+
+ + 档位 {index + 1} + + {form.rewards.length > 1 ? ( + + ) : null} +
+ + updateReward(index, "thresholdCoinSpent", event.target.value) + } + /> + + updateReward(index, "rewardCoinAmount", event.target.value) + } + /> +
+ ))} +
+
+
+
+ + +
+
+
+ + setToast("")} /> +
+ ); +} + +function statusLabel(status) { + return statusOptions.find(([value]) => value === status)?.[1] || status || "-"; +} + +function statusColor(status) { + if (status === "active") return "success"; + if (status === "settled") return "info"; + if (status === "settling") return "warning"; + if (status === "disabled") return "default"; + return "secondary"; +} + +function applicationStatusLabel(status) { + return ( + { + applied: "已申请", + pending: "待发奖", + granted: "已发奖", + failed: "发奖失败", + }[status] || + status || + "-" + ); +} + +function millisToInput(ms) { + if (!ms) return ""; + const local = new Date(ms - new Date(ms).getTimezoneOffset() * 60000); + return local.toISOString().slice(0, 16); +} + +function inputToMillis(value) { + return value ? new Date(value).getTime() : 0; +} + +function normalizeRewardPayload(rewards) { + const payload = (rewards || []).map((reward, index) => { + const thresholdCoinSpent = Number(reward.thresholdCoinSpent || 0); + const rewardCoinAmount = Number(reward.rewardCoinAmount || 0); + if (!Number.isInteger(thresholdCoinSpent) || thresholdCoinSpent <= 0) { + throw new Error("开业流水必须是大于 0 的整数"); + } + if (!Number.isInteger(rewardCoinAmount) || rewardCoinAmount < 0) { + throw new Error("奖励金币不能小于 0"); + } + return { + rank_no: index + 1, + tier_no: index + 1, + threshold_coin_spent: thresholdCoinSpent, + reward_coin_amount: rewardCoinAmount, + }; + }); + for (let index = 1; index < payload.length; index += 1) { + if (payload[index].threshold_coin_spent <= payload[index - 1].threshold_coin_spent) { + throw new Error("开业流水档位必须递增"); + } + } + if (!payload.length) { + throw new Error("至少需要配置一个开业流水档位"); + } + return payload; +} + +function formatNumber(value) { + return Number(value || 0).toLocaleString(); +} diff --git a/src/features/agency-opening/permissions.js b/src/features/agency-opening/permissions.js new file mode 100644 index 0000000..e425314 --- /dev/null +++ b/src/features/agency-opening/permissions.js @@ -0,0 +1,11 @@ +import { PERMISSIONS } from "@/app/permissions"; +import { useAuth } from "@/app/auth/AuthProvider.jsx"; + +export function useAgencyOpeningAbilities() { + const { can } = useAuth(); + return { + canCreate: can(PERMISSIONS.agencyOpeningCreate), + canUpdate: can(PERMISSIONS.agencyOpeningUpdate), + canView: can(PERMISSIONS.agencyOpeningView), + }; +} diff --git a/src/features/agency-opening/routes.js b/src/features/agency-opening/routes.js new file mode 100644 index 0000000..ef99e23 --- /dev/null +++ b/src/features/agency-opening/routes.js @@ -0,0 +1,12 @@ +import { MENU_CODES, PERMISSIONS } from "@/app/permissions"; + +export const agencyOpeningRoutes = [ + { + label: "代理开业活动", + loader: () => import("./pages/AgencyOpeningPage.jsx").then((module) => module.AgencyOpeningPage), + menuCode: MENU_CODES.agencyOpening, + pageKey: "agency-opening", + path: "/activities/agency-opening", + permission: PERMISSIONS.agencyOpeningView, + }, +]; diff --git a/src/features/cp-config/api.ts b/src/features/cp-config/api.ts index ada3cd6..fd5b02b 100644 --- a/src/features/cp-config/api.ts +++ b/src/features/cp-config/api.ts @@ -25,9 +25,25 @@ export interface CPRelationConfigDto { export interface CPConfigDto { appCode: string; relations: CPRelationConfigDto[]; + weeklyRank: CPWeeklyRankDto; serverTimeMs?: number; } +export interface CPWeeklyRankRewardDto { + rankNo: number; + resourceGroupId: number; +} + +export interface CPWeeklyRankDto { + enabled: boolean; + relationType: CPRelationType; + topCount: number; + rewards: CPWeeklyRankRewardDto[]; + updatedByAdminId?: number; + createdAtMs?: number; + updatedAtMs?: number; +} + export interface CPConfigPayload { relations: Array<{ relationType: CPRelationType; @@ -43,6 +59,11 @@ export interface CPConfigPayload { status: string; }>; }>; + weeklyRank: { + enabled: boolean; + topCount: number; + rewards: CPWeeklyRankRewardDto[]; + }; } const path = "/v1/admin/activity/cp/config"; @@ -58,12 +79,14 @@ export function updateCPConfig(payload: CPConfigPayload): Promise { type RawConfig = CPConfigDto & Record; type RawRelation = CPRelationConfigDto & Record; type RawLevel = CPLevelDto & Record; +type RawWeeklyRank = CPWeeklyRankDto & Record; function normalizeConfig(raw: RawConfig): CPConfigDto { const item = asRecord(raw) as RawConfig; return { appCode: stringValue(item.appCode ?? item.app_code), relations: arrayValue(item.relations).map(normalizeRelation), + weeklyRank: normalizeWeeklyRank(item.weeklyRank ?? item.weekly_rank), serverTimeMs: numberValue(item.serverTimeMs ?? item.server_time_ms), }; } @@ -83,6 +106,25 @@ function normalizeRelation(raw: unknown): CPRelationConfigDto { }; } +function normalizeWeeklyRank(raw: unknown): CPWeeklyRankDto { + const item = asRecord(raw) as RawWeeklyRank; + return { + enabled: item.enabled !== false, + relationType: normalizeRelationType(item.relationType ?? item.relation_type), + topCount: numberValue(item.topCount ?? item.top_count) || 3, + rewards: arrayValue(item.rewards).map((reward) => { + const rawReward = asRecord(reward); + return { + rankNo: numberValue(rawReward.rankNo ?? rawReward.rank_no), + resourceGroupId: numberValue(rawReward.resourceGroupId ?? rawReward.resource_group_id), + }; + }), + updatedByAdminId: numberValue(item.updatedByAdminId ?? item.updated_by_admin_id), + createdAtMs: numberValue(item.createdAtMs ?? item.created_at_ms), + updatedAtMs: numberValue(item.updatedAtMs ?? item.updated_at_ms), + }; +} + function normalizeLevel(raw: unknown): CPLevelDto { const item = asRecord(raw) as RawLevel; return { diff --git a/src/features/cp-config/hooks/useCPConfigPage.js b/src/features/cp-config/hooks/useCPConfigPage.js index 062526f..77ea2d1 100644 --- a/src/features/cp-config/hooks/useCPConfigPage.js +++ b/src/features/cp-config/hooks/useCPConfigPage.js @@ -10,6 +10,7 @@ export const cpRelationLabels = { brother: "兄弟", sister: "姐妹", }; +export const cpWeeklyRankRanks = [1, 2, 3]; const defaultExpireHours = 24; const defaultThresholds = [0, 10000, 30000, 60000, 100000]; @@ -17,8 +18,11 @@ const defaultThresholds = [0, 10000, 30000, 60000, 100000]; export function useCPConfigPage() { const abilities = useCPConfigAbilities(); const { showToast } = useToast(); - const [config, setConfig] = useState({ relations: defaultRelations() }); - const [form, setForm] = useState({ relations: defaultRelations().map(relationToForm) }); + const [config, setConfig] = useState({ relations: defaultRelations(), weeklyRank: defaultWeeklyRank() }); + const [form, setForm] = useState({ + relations: defaultRelations().map(relationToForm), + weeklyRank: weeklyRankToForm(defaultWeeklyRank()), + }); const [drawerOpen, setDrawerOpen] = useState(false); const [loading, setLoading] = useState(false); const [resourceGroups, setResourceGroups] = useState([]); @@ -39,8 +43,9 @@ export function useCPConfigPage() { if (configResult.status === "fulfilled") { const remoteConfig = configResult.value; const relations = completeRelations(remoteConfig.relations || []); - setConfig({ ...remoteConfig, relations }); - setForm({ relations: relations.map(relationToForm) }); + const weeklyRank = completeWeeklyRank(remoteConfig.weeklyRank); + setConfig({ ...remoteConfig, relations, weeklyRank }); + setForm({ relations: relations.map(relationToForm), weeklyRank: weeklyRankToForm(weeklyRank) }); } else { showToast(configResult.reason?.message || "加载 CP 配置失败", "error"); } @@ -61,7 +66,10 @@ export function useCPConfigPage() { }, [reload]); const openDrawer = () => { - setForm({ relations: completeRelations(config.relations || []).map(relationToForm) }); + setForm({ + relations: completeRelations(config.relations || []).map(relationToForm), + weeklyRank: weeklyRankToForm(completeWeeklyRank(config.weeklyRank)), + }); setDrawerOpen(true); }; @@ -69,7 +77,10 @@ export function useCPConfigPage() { if (saving) { return; } - setForm({ relations: completeRelations(config.relations || []).map(relationToForm) }); + setForm({ + relations: completeRelations(config.relations || []).map(relationToForm), + weeklyRank: weeklyRankToForm(completeWeeklyRank(config.weeklyRank)), + }); setDrawerOpen(false); }; @@ -98,6 +109,25 @@ export function useCPConfigPage() { })); }; + const updateWeeklyRank = (patch) => { + setForm((current) => ({ + ...current, + weeklyRank: { ...(current.weeklyRank || weeklyRankToForm(defaultWeeklyRank())), ...patch }, + })); + }; + + const updateWeeklyRankReward = (rankNo, patch) => { + setForm((current) => ({ + ...current, + weeklyRank: { + ...(current.weeklyRank || weeklyRankToForm(defaultWeeklyRank())), + rewards: completeWeeklyRankRewardForms(current.weeklyRank?.rewards || []).map((reward) => + Number(reward.rankNo) === Number(rankNo) ? { ...reward, ...patch } : reward, + ), + }, + })); + }; + const submit = async (event) => { event.preventDefault(); if (!abilities.canUpdate) { @@ -114,8 +144,9 @@ export function useCPConfigPage() { try { const saved = await updateCPConfig(payload); const relations = completeRelations(saved.relations || []); - setConfig({ ...saved, relations }); - setForm({ relations: relations.map(relationToForm) }); + const weeklyRank = completeWeeklyRank(saved.weeklyRank); + setConfig({ ...saved, relations, weeklyRank }); + setForm({ relations: relations.map(relationToForm), weeklyRank: weeklyRankToForm(weeklyRank) }); setDrawerOpen(false); showToast("CP 配置已保存", "success"); } catch (err) { @@ -140,6 +171,8 @@ export function useCPConfigPage() { submit, updateLevel, updateRelation, + updateWeeklyRank, + updateWeeklyRankReward, }; } @@ -167,6 +200,16 @@ function defaultLevels() { })); } +function defaultWeeklyRank() { + return { + enabled: true, + relationType: "cp", + topCount: 3, + rewards: cpWeeklyRankRanks.map((rankNo) => ({ rankNo, resourceGroupId: 0 })), + updatedAtMs: 0, + }; +} + function completeRelations(relations) { const byType = new Map((relations || []).map((relation) => [relation.relationType, relation])); return defaultRelations().map((fallback) => { @@ -211,6 +254,44 @@ function levelToForm(level) { }; } +function completeWeeklyRank(weeklyRank) { + const fallback = defaultWeeklyRank(); + const normalized = { ...fallback, ...(weeklyRank || {}) }; + normalized.topCount = 3; + normalized.relationType = "cp"; + normalized.rewards = completeWeeklyRankRewards(normalized.rewards || []); + return normalized; +} + +function completeWeeklyRankRewards(rewards) { + const byRank = new Map((rewards || []).map((reward) => [Number(reward.rankNo), reward])); + return cpWeeklyRankRanks.map((rankNo) => ({ + rankNo, + resourceGroupId: Number(byRank.get(rankNo)?.resourceGroupId || 0), + })); +} + +function weeklyRankToForm(weeklyRank) { + const completed = completeWeeklyRank(weeklyRank); + return { + enabled: completed.enabled !== false, + relationType: "cp", + topCount: "3", + rewards: completeWeeklyRankRewards(completed.rewards).map((reward) => ({ + rankNo: reward.rankNo, + resourceGroupId: reward.resourceGroupId ? String(reward.resourceGroupId) : "", + })), + }; +} + +function completeWeeklyRankRewardForms(rewards) { + const byRank = new Map((rewards || []).map((reward) => [Number(reward.rankNo), reward])); + return cpWeeklyRankRanks.map((rankNo) => ({ + rankNo, + resourceGroupId: byRank.get(rankNo)?.resourceGroupId || "", + })); +} + function payloadFromForm(form) { const relations = completeFormRelations(form.relations || []).map((relation) => { const relationType = relation.relationType; @@ -236,7 +317,29 @@ function payloadFromForm(form) { levels: completeFormLevels(relation.levels || [], relationType), }; }); - return { relations }; + const weeklyRank = weeklyRankPayloadFromForm(form.weeklyRank || weeklyRankToForm(defaultWeeklyRank())); + return { relations, weeklyRank }; +} + +function weeklyRankPayloadFromForm(weeklyRank) { + const enabled = weeklyRank.enabled !== false; + const rewards = completeWeeklyRankRewardForms(weeklyRank.rewards || []).map((reward) => ({ + rankNo: Number(reward.rankNo), + resourceGroupId: Number(reward.resourceGroupId || 0), + })); + if (enabled) { + const missing = rewards.find( + (reward) => !Number.isInteger(reward.resourceGroupId) || reward.resourceGroupId <= 0, + ); + if (missing) { + throw new Error(`CP周榜 Top${missing.rankNo} 奖励资源组必须配置`); + } + } + return { + enabled, + topCount: 3, + rewards, + }; } function completeFormRelations(relations) { diff --git a/src/features/cp-config/pages/CPConfigPage.jsx b/src/features/cp-config/pages/CPConfigPage.jsx index 9190145..60de32a 100644 --- a/src/features/cp-config/pages/CPConfigPage.jsx +++ b/src/features/cp-config/pages/CPConfigPage.jsx @@ -4,7 +4,7 @@ import SaveOutlined from "@mui/icons-material/SaveOutlined"; import Drawer from "@mui/material/Drawer"; import TextField from "@mui/material/TextField"; import { useMemo } from "react"; -import { cpRelationLabels, useCPConfigPage } from "@/features/cp-config/hooks/useCPConfigPage.js"; +import { cpRelationLabels, cpWeeklyRankRanks, useCPConfigPage } from "@/features/cp-config/hooks/useCPConfigPage.js"; import styles from "@/features/cp-config/cp-config.module.css"; import { AdminListBody, AdminListPage } from "@/shared/ui/AdminListLayout.jsx"; import { AdminSwitch } from "@/shared/ui/AdminSwitch.jsx"; @@ -28,6 +28,9 @@ export function CPConfigPage() { {relationMax(relations, "brother")} {relationMax(relations, "sister")} {relationBreakupCost(relations, "cp")} + + {page.config.weeklyRank?.enabled === false ? "停用" : "启用"} +
+ ) : null + } + filters={null} + /> +
+
+
+ +

全服通知

+
+
+ + {messageTypes.map((item) => ( + + {item.label} + + ))} + + + {targetScopes.map((item) => ( + + {item.label} + + ))} + + {form.targetScope === "single_user" ? ( + + ) : null} + {form.targetScope === "user_ids" ? ( + + ) : null} + {form.targetScope === "region" ? ( + + ) : null} + {form.targetScope === "country" ? ( + + ) : null} + + +
+
+ + + +
+
+ + {actionTypes.map((item) => ( + + {item.label} + + ))} + + + + + + + + +
+
+
+

{form.title || "标题"}

+

{form.summary || "摘要"}

+
+ {messageTypes.find((item) => item.value === form.messageType)?.label} + {targetLabel} + {form.actionType || "无跳转"} +
+ {result ? ( + + {result.status} · {result.job_id} · {result.command_id} + + ) : null} +
+
+ + ); +} + +function buildPayload(form) { + const title = form.title.trim(); + const summary = form.summary.trim(); + if (!title || !summary) { + return { error: "标题和摘要不能为空", ok: false }; + } + const batchSize = parsePositiveNumber(form.batchSize, "批量大小"); + if (!batchSize.ok) { + return batchSize; + } + const priority = parseOptionalNumber(form.priority, "优先级"); + if (!priority.ok) { + return priority; + } + const expireAtMS = datetimeLocalToMS(form.expireAt); + if (!expireAtMS.ok) { + return expireAtMS; + } + const target = buildTargetPayload(form); + if (!target.ok) { + return target; + } + const metadataJson = form.metadataJson.trim(); + if (metadataJson) { + try { + JSON.parse(metadataJson); + } catch { + return { error: "Metadata JSON 不正确", ok: false }; + } + } + return { + ok: true, + value: { + ...target.value, + action_param: form.actionParam.trim() || undefined, + action_type: form.actionType, + aggregate_id: form.aggregateId.trim() || undefined, + aggregate_type: form.aggregateType.trim() || undefined, + batch_size: batchSize.value, + body: form.body.trim() || undefined, + command_id: form.commandId.trim() || undefined, + expire_at_ms: expireAtMS.value || undefined, + message_type: form.messageType, + metadata_json: metadataJson || undefined, + priority: priority.value, + producer_event_type: form.producerEventType.trim() || undefined, + summary, + target_scope: form.targetScope, + title, + }, + }; +} + +function buildTargetPayload(form) { + if (form.targetScope === "single_user") { + const targetUserID = parsePositiveNumber(form.targetUserId, "用户 ID"); + return targetUserID.ok ? { ok: true, value: { target_user_id: targetUserID.value } } : targetUserID; + } + if (form.targetScope === "user_ids") { + const ids = form.userIds + .split(/[\s,,]+/) + .map((item) => Number(item.trim())) + .filter((value) => Number.isInteger(value) && value > 0); + if (!ids.length) { + return { error: "用户 ID 列表不能为空", ok: false }; + } + return { ok: true, value: { user_ids: Array.from(new Set(ids)) } }; + } + if (form.targetScope === "region") { + const regionID = parsePositiveNumber(form.regionId, "区域 ID"); + return regionID.ok ? { ok: true, value: { region_id: regionID.value } } : regionID; + } + if (form.targetScope === "country") { + const country = form.country.trim(); + return country ? { ok: true, value: { country } } : { error: "国家不能为空", ok: false }; + } + return { ok: true, value: {} }; +} + +function parsePositiveNumber(value, label) { + const number = Number(String(value || "").trim()); + if (!Number.isInteger(number) || number <= 0) { + return { error: `${label} 必须是正整数`, ok: false }; + } + return { ok: true, value: number }; +} + +function parseOptionalNumber(value, label) { + const raw = String(value ?? "").trim(); + if (!raw) { + return { ok: true, value: 0 }; + } + const number = Number(raw); + if (!Number.isInteger(number)) { + return { error: `${label} 必须是整数`, ok: false }; + } + return { ok: true, value: number }; +} + +function datetimeLocalToMS(value) { + if (!value) { + return { ok: true, value: 0 }; + } + const ms = new Date(value).getTime(); + if (!Number.isFinite(ms)) { + return { error: "过期时间不正确", ok: false }; + } + return { ok: true, value: ms }; +} diff --git a/src/features/operations/permissions.js b/src/features/operations/permissions.js index 8906226..ce28298 100644 --- a/src/features/operations/permissions.js +++ b/src/features/operations/permissions.js @@ -18,3 +18,12 @@ export function useGiftDiamondAbilities() { canView: can(PERMISSIONS.giftDiamondView), }; } + +export function useFullServerNoticeAbilities() { + const { can } = useAuth(); + + return { + canSend: can(PERMISSIONS.fullServerNoticeSend), + canView: can(PERMISSIONS.fullServerNoticeView), + }; +} diff --git a/src/features/operations/routes.js b/src/features/operations/routes.js index 096ee78..a9b29e4 100644 --- a/src/features/operations/routes.js +++ b/src/features/operations/routes.js @@ -41,4 +41,12 @@ export const operationsRoutes = [ path: "/operations/gift-diamonds", permission: PERMISSIONS.giftDiamondView, }, + { + label: "全服通知", + loader: () => import("./pages/FullServerNoticePage.jsx").then((module) => module.FullServerNoticePage), + menuCode: MENU_CODES.operationFullServerNotice, + pageKey: "operation-full-server-notice", + path: "/operations/full-server-notices", + permission: PERMISSIONS.fullServerNoticeView, + }, ]; diff --git a/src/features/roles/hooks/useRolesPage.js b/src/features/roles/hooks/useRolesPage.js index 064f70c..05fd6ca 100644 --- a/src/features/roles/hooks/useRolesPage.js +++ b/src/features/roles/hooks/useRolesPage.js @@ -39,6 +39,7 @@ const permissionGroupDefinitions = [ key: "activities", prefixes: [ "achievement", + "agency-opening", "cp-config", "cumulative-recharge-reward", "daily-task", diff --git a/src/shared/api/generated/endpoints.ts b/src/shared/api/generated/endpoints.ts index 7040a6c..f0f7803 100644 --- a/src/shared/api/generated/endpoints.ts +++ b/src/shared/api/generated/endpoints.ts @@ -39,6 +39,7 @@ export const API_OPERATIONS = { createCountry: "createCountry", createEmojiPack: "createEmojiPack", createExploreTab: "createExploreTab", + createFullServerNoticeFanout: "createFullServerNoticeFanout", createGift: "createGift", createH5Link: "createH5Link", createHostAgencyPolicy: "createHostAgencyPolicy", @@ -102,6 +103,7 @@ export const API_OPERATIONS = { generatePrettyIds: "generatePrettyIds", getCoinSellerSalaryRates: "getCoinSellerSalaryRates", getCountry: "getCountry", + getCPConfig: "getCPConfig", getCumulativeRechargeRewardConfig: "getCumulativeRechargeRewardConfig", getFirstRechargeRewardConfig: "getFirstRechargeRewardConfig", getInviteActivityRewardConfig: "getInviteActivityRewardConfig", @@ -230,6 +232,7 @@ export const API_OPERATIONS = { updateBanner: "updateBanner", updateCatalog: "updateCatalog", updateCountry: "updateCountry", + updateCPConfig: "updateCPConfig", updateCumulativeRechargeRewardConfig: "updateCumulativeRechargeRewardConfig", updateDiceConfig: "updateDiceConfig", updateExploreTab: "updateExploreTab", @@ -460,6 +463,13 @@ export const API_ENDPOINTS: Record = { permission: "app-config:update", permissions: ["app-config:update"] }, + createFullServerNoticeFanout: { + method: "POST", + operationId: API_OPERATIONS.createFullServerNoticeFanout, + path: "/v1/admin/operations/full-server-notices/fanout", + permission: "full-server-notice:send", + permissions: ["full-server-notice:send"] + }, createGift: { method: "POST", operationId: API_OPERATIONS.createGift, @@ -901,6 +911,13 @@ export const API_ENDPOINTS: Record = { permission: "country:view", permissions: ["country:view"] }, + getCPConfig: { + method: "GET", + operationId: API_OPERATIONS.getCPConfig, + path: "/v1/admin/activity/cp/config", + permission: "cp-config:view", + permissions: ["cp-config:view"] + }, getCumulativeRechargeRewardConfig: { method: "GET", operationId: API_OPERATIONS.getCumulativeRechargeRewardConfig, @@ -1783,6 +1800,13 @@ export const API_ENDPOINTS: Record = { permission: "country:update", permissions: ["country:update"] }, + updateCPConfig: { + method: "PUT", + operationId: API_OPERATIONS.updateCPConfig, + path: "/v1/admin/activity/cp/config", + permission: "cp-config:update", + permissions: ["cp-config:update"] + }, updateCumulativeRechargeRewardConfig: { method: "PUT", operationId: API_OPERATIONS.updateCumulativeRechargeRewardConfig, diff --git a/src/shared/api/generated/schema.d.ts b/src/shared/api/generated/schema.d.ts index d811b82..558713d 100644 --- a/src/shared/api/generated/schema.d.ts +++ b/src/shared/api/generated/schema.d.ts @@ -100,6 +100,22 @@ export interface paths { patch?: never; trace?: never; }; + "/admin/activity/cp/config": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["getCPConfig"]; + put: operations["updateCPConfig"]; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/admin/activity/red-packets": { parameters: { query?: never; @@ -1636,6 +1652,22 @@ export interface paths { patch?: never; trace?: never; }; + "/admin/operations/full-server-notices/fanout": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["createFullServerNoticeFanout"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/admin/operations/reports": { parameters: { query?: never; @@ -3597,6 +3629,30 @@ export interface operations { 200: components["responses"]["EmptyResponse"]; }; }; + getCPConfig: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: components["responses"]["EmptyResponse"]; + }; + }; + updateCPConfig: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: components["responses"]["EmptyResponse"]; + }; + }; listRedPackets: { parameters: { query?: never; @@ -5443,6 +5499,51 @@ export interface operations { 200: components["responses"]["EmptyResponse"]; }; }; + createFullServerNoticeFanout: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + action_param?: string; + action_type?: string; + aggregate_id?: string; + aggregate_type?: string; + batch_size?: number; + body?: string; + command_id?: string; + country?: string; + /** Format: int64 */ + expire_at_ms?: number; + icon_url?: string; + image_url?: string; + /** @enum {string} */ + message_type: "system" | "activity"; + metadata_json?: string; + priority?: number; + producer_event_type?: string; + /** Format: int64 */ + region_id?: number; + /** Format: int64 */ + sent_at_ms?: number; + summary: string; + /** @enum {string} */ + target_scope: "all_active_users" | "single_user" | "user_ids" | "region" | "country"; + /** Format: int64 */ + target_user_id?: number; + title: string; + user_ids?: number[]; + }; + }; + }; + responses: { + 201: components["responses"]["EmptyResponse"]; + }; + }; listReports: { parameters: { query?: never;