feat(reports, risk, deployment): 支持开注商经营报表、风险池按开注商操作并调整 PM2 部署目录
Some checks failed
lotteryadmin CI / build (push) Has been cancelled
Some checks failed
lotteryadmin CI / build (push) Has been cancelled
This commit is contained in:
@@ -10,12 +10,12 @@ REMOTE_DIR="/www/wwwroot/lotteryAdmin"
|
|||||||
echo "🔨 构建中..."
|
echo "🔨 构建中..."
|
||||||
npm run build
|
npm run build
|
||||||
|
|
||||||
echo "📦 上传到服务器..."
|
echo "📦 上传到服务器 app/ ..."
|
||||||
ssh ${SERVER} "mkdir -p ${REMOTE_DIR}/.next/standalone ${REMOTE_DIR}/logs"
|
ssh ${SERVER} "mkdir -p ${REMOTE_DIR}/app ${REMOTE_DIR}/logs"
|
||||||
rsync -avz --delete .next/standalone/ ${SERVER}:${REMOTE_DIR}/.next/standalone/
|
rsync -az --delete .next/standalone/ ${SERVER}:${REMOTE_DIR}/app/
|
||||||
rsync -avz ecosystem.config.cjs ${SERVER}:${REMOTE_DIR}/ecosystem.config.cjs
|
rsync -az ecosystem.config.cjs ${SERVER}:${REMOTE_DIR}/ecosystem.config.cjs
|
||||||
|
|
||||||
echo "🚀 服务器重启 PM2..."
|
echo "🚀 重启 PM2..."
|
||||||
ssh ${SERVER} "source /etc/profile && source ~/.bashrc && mkdir -p ${REMOTE_DIR}/logs && cd ${REMOTE_DIR} && (pm2 restart lotteryadmin 2>/dev/null || pm2 start ecosystem.config.cjs) && pm2 save"
|
ssh ${SERVER} "source /etc/profile && source ~/.bashrc && cd ${REMOTE_DIR} && (pm2 delete lotteryadmin 2>/dev/null || true) && pm2 start ecosystem.config.cjs --only lotteryadmin --update-env && pm2 save"
|
||||||
|
|
||||||
echo "✅ lotteryAdmin 部署完成!"
|
echo "✅ lotteryAdmin 部署完成!"
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import type {
|
|||||||
AdminReportDailyProfitRow,
|
AdminReportDailyProfitRow,
|
||||||
AdminReportListData,
|
AdminReportListData,
|
||||||
AdminReportPlayDimensionRow,
|
AdminReportPlayDimensionRow,
|
||||||
|
AdminReportProviderProfitRow,
|
||||||
AdminReportPlayerWinLossRow,
|
AdminReportPlayerWinLossRow,
|
||||||
AdminReportQueryParams,
|
AdminReportQueryParams,
|
||||||
} from "@/types/api/admin-reports";
|
} from "@/types/api/admin-reports";
|
||||||
@@ -19,6 +20,12 @@ export async function getAdminReportDailyProfit(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getAdminReportProviderProfit(
|
||||||
|
params: AdminReportQueryParams,
|
||||||
|
): Promise<AdminReportListData<AdminReportProviderProfitRow>> {
|
||||||
|
return adminRequest.get<AdminReportListData<AdminReportProviderProfitRow>>(`${A}/reports/provider-profit`, { params });
|
||||||
|
}
|
||||||
|
|
||||||
export async function getAdminReportPlayerWinLoss(
|
export async function getAdminReportPlayerWinLoss(
|
||||||
params: AdminReportQueryParams,
|
params: AdminReportQueryParams,
|
||||||
): Promise<AdminReportListData<AdminReportPlayerWinLossRow>> {
|
): Promise<AdminReportListData<AdminReportPlayerWinLossRow>> {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ const A = `/admin`;
|
|||||||
export type AdminRiskPoolListQuery = {
|
export type AdminRiskPoolListQuery = {
|
||||||
page?: number;
|
page?: number;
|
||||||
per_page?: number;
|
per_page?: number;
|
||||||
|
provider_code?: string;
|
||||||
sold_out_only?: boolean;
|
sold_out_only?: boolean;
|
||||||
high_risk_only?: boolean;
|
high_risk_only?: boolean;
|
||||||
active_only?: boolean;
|
active_only?: boolean;
|
||||||
@@ -27,6 +28,7 @@ export async function getAdminRiskPools(
|
|||||||
params: {
|
params: {
|
||||||
page: q.page,
|
page: q.page,
|
||||||
per_page: q.per_page,
|
per_page: q.per_page,
|
||||||
|
provider_code: q.provider_code || undefined,
|
||||||
sold_out_only: q.sold_out_only === true ? 1 : undefined,
|
sold_out_only: q.sold_out_only === true ? 1 : undefined,
|
||||||
high_risk_only: q.high_risk_only === true ? 1 : undefined,
|
high_risk_only: q.high_risk_only === true ? 1 : undefined,
|
||||||
active_only: q.active_only === true ? 1 : undefined,
|
active_only: q.active_only === true ? 1 : undefined,
|
||||||
@@ -39,26 +41,33 @@ export async function getAdminRiskPools(
|
|||||||
export async function postAdminRiskPoolManualClose(
|
export async function postAdminRiskPoolManualClose(
|
||||||
drawId: number,
|
drawId: number,
|
||||||
number4d: string,
|
number4d: string,
|
||||||
|
providerCode: string,
|
||||||
): Promise<AdminRiskPoolRow> {
|
): Promise<AdminRiskPoolRow> {
|
||||||
const encoded = encodeURIComponent(number4d);
|
const encoded = encodeURIComponent(number4d);
|
||||||
return adminRequest.post<AdminRiskPoolRow>(
|
return adminRequest.post<AdminRiskPoolRow>(
|
||||||
`${A}/draws/${drawId}/risk-pools/${encoded}/manual-close`,
|
`${A}/draws/${drawId}/risk-pools/${encoded}/manual-close`,
|
||||||
|
undefined,
|
||||||
|
{ params: { provider_code: providerCode || undefined } },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function postAdminRiskPoolRecover(
|
export async function postAdminRiskPoolRecover(
|
||||||
drawId: number,
|
drawId: number,
|
||||||
number4d: string,
|
number4d: string,
|
||||||
|
providerCode: string,
|
||||||
): Promise<AdminRiskPoolRow> {
|
): Promise<AdminRiskPoolRow> {
|
||||||
const encoded = encodeURIComponent(number4d);
|
const encoded = encodeURIComponent(number4d);
|
||||||
return adminRequest.post<AdminRiskPoolRow>(
|
return adminRequest.post<AdminRiskPoolRow>(
|
||||||
`${A}/draws/${drawId}/risk-pools/${encoded}/recover`,
|
`${A}/draws/${drawId}/risk-pools/${encoded}/recover`,
|
||||||
|
undefined,
|
||||||
|
{ params: { provider_code: providerCode || undefined } },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AdminRiskPoolShowQuery = {
|
export type AdminRiskPoolShowQuery = {
|
||||||
page?: number;
|
page?: number;
|
||||||
per_page?: number;
|
per_page?: number;
|
||||||
|
provider_code?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function getAdminRiskPoolDetail(
|
export async function getAdminRiskPoolDetail(
|
||||||
@@ -69,6 +78,6 @@ export async function getAdminRiskPoolDetail(
|
|||||||
const encoded = encodeURIComponent(number4d);
|
const encoded = encodeURIComponent(number4d);
|
||||||
return adminRequest.get<AdminRiskPoolShowData>(
|
return adminRequest.get<AdminRiskPoolShowData>(
|
||||||
`${A}/draws/${drawId}/risk-pools/${encoded}`,
|
`${A}/draws/${drawId}/risk-pools/${encoded}`,
|
||||||
{ params: { page: q.page, per_page: q.per_page } },
|
{ params: { page: q.page, per_page: q.per_page, provider_code: q.provider_code || undefined } },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,12 @@ import { RiskPoolDetailConsole } from "@/modules/risk/risk-pool-detail-console";
|
|||||||
|
|
||||||
export default async function AdminDrawRiskPoolDetailPage(props: {
|
export default async function AdminDrawRiskPoolDetailPage(props: {
|
||||||
params: Promise<{ drawId: string; number: string }>;
|
params: Promise<{ drawId: string; number: string }>;
|
||||||
|
searchParams: Promise<{ provider_code?: string }>;
|
||||||
}) {
|
}) {
|
||||||
const { drawId, number } = await props.params;
|
const { drawId, number } = await props.params;
|
||||||
|
const { provider_code } = await props.searchParams;
|
||||||
const id = Number(drawId);
|
const id = Number(drawId);
|
||||||
|
const providerCode = (provider_code ?? "SG").trim().toUpperCase() || "SG";
|
||||||
|
|
||||||
return <RiskPoolDetailConsole drawId={id} number4d={number} />;
|
return <RiskPoolDetailConsole drawId={id} number4d={number} providerCode={providerCode} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ import { redirect } from "next/navigation";
|
|||||||
|
|
||||||
export default async function AdminRiskPoolDetailPage(props: {
|
export default async function AdminRiskPoolDetailPage(props: {
|
||||||
params: Promise<{ drawId: string; number: string }>;
|
params: Promise<{ drawId: string; number: string }>;
|
||||||
|
searchParams: Promise<{ provider_code?: string }>;
|
||||||
}) {
|
}) {
|
||||||
const { drawId, number } = await props.params;
|
const { drawId, number } = await props.params;
|
||||||
redirect(`/admin/draws/${drawId}/risk/pools/${number}`);
|
const { provider_code } = await props.searchParams;
|
||||||
|
const suffix = provider_code ? `?provider_code=${encodeURIComponent(provider_code)}` : "";
|
||||||
|
redirect(`/admin/draws/${drawId}/risk/pools/${number}${suffix}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -316,6 +316,10 @@
|
|||||||
"title": "Play dimension report",
|
"title": "Play dimension report",
|
||||||
"summary": "P&L by play and business date; defaults to the last 30 days. Ticket amounts — not credit-line settlement."
|
"summary": "P&L by play and business date; defaults to the last 30 days. Ticket amounts — not credit-line settlement."
|
||||||
},
|
},
|
||||||
|
"provider_profit": {
|
||||||
|
"title": "Bet provider performance",
|
||||||
|
"summary": "Ticket items, bets, payouts, and platform gross by provider and business date; defaults to the last 30 days."
|
||||||
|
},
|
||||||
"sold_out_number": {
|
"sold_out_number": {
|
||||||
"title": "Sold-out number report",
|
"title": "Sold-out number report",
|
||||||
"summary": "Review sold-out numbers, sold-out time, and risk lock state by draw."
|
"summary": "Review sold-out numbers, sold-out time, and risk lock state by draw."
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
"searchNumber": "Search number",
|
"searchNumber": "Search number",
|
||||||
"searchNumberPlaceholder": "For example 8888",
|
"searchNumberPlaceholder": "For example 8888",
|
||||||
"riskFilter": "Risk filter",
|
"riskFilter": "Risk filter",
|
||||||
|
"provider": "Provider",
|
||||||
"sort": "Sort",
|
"sort": "Sort",
|
||||||
"filterAll": "All",
|
"filterAll": "All",
|
||||||
"filterActive": "Active / high risk",
|
"filterActive": "Active / high risk",
|
||||||
@@ -51,9 +52,9 @@
|
|||||||
"actionFailed": "Action failed",
|
"actionFailed": "Action failed",
|
||||||
"confirm": {
|
"confirm": {
|
||||||
"closeTitle": "Confirm close number?",
|
"closeTitle": "Confirm close number?",
|
||||||
"closeDescription": "Number {{number}} will be blocked for this draw.",
|
"closeDescription": "Number {{number}} ({{provider}}) will be blocked for this draw.",
|
||||||
"recoverTitle": "Confirm recover number?",
|
"recoverTitle": "Confirm recover number?",
|
||||||
"recoverDescription": "Number {{number}} will be open for betting again."
|
"recoverDescription": "Number {{number}} ({{provider}}) will be open for betting again."
|
||||||
},
|
},
|
||||||
"detailTitle": "Risk pool details",
|
"detailTitle": "Risk pool details",
|
||||||
"loadDetailFailed": "Failed to load risk pool details",
|
"loadDetailFailed": "Failed to load risk pool details",
|
||||||
|
|||||||
@@ -68,6 +68,10 @@
|
|||||||
"summary": "खेल र व्यावसायिक मितिअनुसार P&L; मिति नचयेमा पछिल्लो ३० दिन।",
|
"summary": "खेल र व्यावसायिक मितिअनुसार P&L; मिति नचयेमा पछिल्लो ३० दिन।",
|
||||||
"title": "खेल आयाम रिपोर्ट"
|
"title": "खेल आयाम रिपोर्ट"
|
||||||
},
|
},
|
||||||
|
"provider_profit": {
|
||||||
|
"summary": "खुलाउने कम्पनी र व्यावसायिक मितिअनुसार टिकट, बेट, पेआउट र प्लेटफर्म नाफा; मिति नचयेमा पछिल्लो ३० दिन।",
|
||||||
|
"title": "खुलाउने कम्पनी सञ्चालन रिपोर्ट"
|
||||||
|
},
|
||||||
"player_transfer": {
|
"player_transfer": {
|
||||||
"summary": "वालेट-मोड मुख्य साइट ट्रान्सफर, रेकर्ड समय अनुसार; मिति नचयेमा पछिल्लो ३० दिन।",
|
"summary": "वालेट-मोड मुख्य साइट ट्रान्सफर, रेकर्ड समय अनुसार; मिति नचयेमा पछिल्लो ३० दिन।",
|
||||||
"title": "खेलाडी ट्रान्सफर रिपोर्ट"
|
"title": "खेलाडी ट्रान्सफर रिपोर्ट"
|
||||||
|
|||||||
@@ -16,9 +16,9 @@
|
|||||||
"closeTime": "बन्द समय",
|
"closeTime": "बन्द समय",
|
||||||
"combinationCount": "संयोजन",
|
"combinationCount": "संयोजन",
|
||||||
"confirm": {
|
"confirm": {
|
||||||
"closeDescription": "नम्बर {{number}} यो ड्रका लागि ब्लक हुनेछ।",
|
"closeDescription": "नम्बर {{number}} ({{provider}}) यो ड्रका लागि ब्लक हुनेछ।",
|
||||||
"closeTitle": "नम्बर बन्द गर्ने?",
|
"closeTitle": "नम्बर बन्द गर्ने?",
|
||||||
"recoverDescription": "नम्बर {{number}} फेरि बाजीका लागि खुल्नेछ।",
|
"recoverDescription": "नम्बर {{number}} ({{provider}}) फेरि बाजीका लागि खुल्नेछ।",
|
||||||
"recoverTitle": "नम्बर पुन: खोल्ने?"
|
"recoverTitle": "नम्बर पुन: खोल्ने?"
|
||||||
},
|
},
|
||||||
"databaseStatus": "डेटाबेस स्थिति",
|
"databaseStatus": "डेटाबेस स्थिति",
|
||||||
@@ -69,6 +69,7 @@
|
|||||||
"remainingAmount": "बाँकी",
|
"remainingAmount": "बाँकी",
|
||||||
"remainingSellable": "बाँकी बिक्रीयोग्य",
|
"remainingSellable": "बाँकी बिक्रीयोग्य",
|
||||||
"riskFilter": "जोखिम फिल्टर",
|
"riskFilter": "जोखिम फिल्टर",
|
||||||
|
"provider": "प्रोभाइडर",
|
||||||
"search": "खोज",
|
"search": "खोज",
|
||||||
"searchNumber": "नम्बर खोज्नुहोस्",
|
"searchNumber": "नम्बर खोज्नुहोस्",
|
||||||
"searchNumberPlaceholder": "जस्तै 8888",
|
"searchNumberPlaceholder": "जस्तै 8888",
|
||||||
|
|||||||
@@ -67,6 +67,7 @@
|
|||||||
"transfer_orders_daily": "转账订单(日)",
|
"transfer_orders_daily": "转账订单(日)",
|
||||||
"hot_number_risk_report": "热门号码风险",
|
"hot_number_risk_report": "热门号码风险",
|
||||||
"play_dimension_report": "玩法维度",
|
"play_dimension_report": "玩法维度",
|
||||||
|
"provider_profit_report": "开注商经营",
|
||||||
"sold_out_number_report": "售罄号码",
|
"sold_out_number_report": "售罄号码",
|
||||||
"audit_operation_report": "后台操作审计"
|
"audit_operation_report": "后台操作审计"
|
||||||
},
|
},
|
||||||
@@ -316,6 +317,10 @@
|
|||||||
"title": "玩法维度报表",
|
"title": "玩法维度报表",
|
||||||
"summary": "按玩法与业务日拆分盈亏;未选日期默认近 30 天。注单口径,非信用账期结算。"
|
"summary": "按玩法与业务日拆分盈亏;未选日期默认近 30 天。注单口径,非信用账期结算。"
|
||||||
},
|
},
|
||||||
|
"provider_profit": {
|
||||||
|
"title": "开注商经营报表",
|
||||||
|
"summary": "按开注商与业务日汇总注项、投注、派彩和平台毛利;未选日期默认近 30 天。"
|
||||||
|
},
|
||||||
"sold_out_number": {
|
"sold_out_number": {
|
||||||
"title": "售罄号码报表",
|
"title": "售罄号码报表",
|
||||||
"summary": "查看单期已售罄号码、售罄时间和风险封锁情况。"
|
"summary": "查看单期已售罄号码、售罄时间和风险封锁情况。"
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
"searchNumber": "搜索号码",
|
"searchNumber": "搜索号码",
|
||||||
"searchNumberPlaceholder": "如 8888",
|
"searchNumberPlaceholder": "如 8888",
|
||||||
"riskFilter": "风险筛选",
|
"riskFilter": "风险筛选",
|
||||||
|
"provider": "开注商",
|
||||||
"sort": "排序",
|
"sort": "排序",
|
||||||
"filterAll": "全部",
|
"filterAll": "全部",
|
||||||
"filterActive": "有占用/高风险",
|
"filterActive": "有占用/高风险",
|
||||||
@@ -51,9 +52,9 @@
|
|||||||
"actionFailed": "操作失败",
|
"actionFailed": "操作失败",
|
||||||
"confirm": {
|
"confirm": {
|
||||||
"closeTitle": "确认关闭该号码下注?",
|
"closeTitle": "确认关闭该号码下注?",
|
||||||
"closeDescription": "号码 {{number}} 在本期将被禁止下注。",
|
"closeDescription": "号码 {{number}}({{provider}})在本期将被禁止下注。",
|
||||||
"recoverTitle": "确认恢复该号码下注?",
|
"recoverTitle": "确认恢复该号码下注?",
|
||||||
"recoverDescription": "号码 {{number}} 将恢复为可下注状态。"
|
"recoverDescription": "号码 {{number}}({{provider}})将恢复为可下注状态。"
|
||||||
},
|
},
|
||||||
"detailTitle": "风险池详情",
|
"detailTitle": "风险池详情",
|
||||||
"loadDetailFailed": "加载风险池详情失败",
|
"loadDetailFailed": "加载风险池详情失败",
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export type ReportUiKey =
|
|||||||
| "player_transfer"
|
| "player_transfer"
|
||||||
| "hot_number_risk"
|
| "hot_number_risk"
|
||||||
| "play_dimension"
|
| "play_dimension"
|
||||||
|
| "provider_profit"
|
||||||
| "sold_out_number"
|
| "sold_out_number"
|
||||||
| "admin_audit";
|
| "admin_audit";
|
||||||
|
|
||||||
@@ -28,6 +29,7 @@ export const REPORT_UI_TO_JOB_TYPE: Record<ReportUiKey, string> = {
|
|||||||
player_transfer: "wallet_transfer_report",
|
player_transfer: "wallet_transfer_report",
|
||||||
hot_number_risk: "hot_number_risk_report",
|
hot_number_risk: "hot_number_risk_report",
|
||||||
play_dimension: "play_dimension_report",
|
play_dimension: "play_dimension_report",
|
||||||
|
provider_profit: "provider_profit_report",
|
||||||
sold_out_number: "sold_out_number_report",
|
sold_out_number: "sold_out_number_report",
|
||||||
admin_audit: "audit_operation_report",
|
admin_audit: "audit_operation_report",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ type DraftRiskRow = Omit<RiskCapItemRow, "id"> & { clientKey: string };
|
|||||||
function newRow(): DraftRiskRow {
|
function newRow(): DraftRiskRow {
|
||||||
return {
|
return {
|
||||||
clientKey: `new-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
clientKey: `new-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
||||||
|
provider_code: "GLOBAL",
|
||||||
draw_id: null,
|
draw_id: null,
|
||||||
normalized_number: "0000",
|
normalized_number: "0000",
|
||||||
cap_amount: 0,
|
cap_amount: 0,
|
||||||
@@ -79,6 +80,7 @@ function isDefaultRiskRow(row: DraftRiskRow): boolean {
|
|||||||
function defaultRiskRowFromAmount(amount: number): DraftRiskRow {
|
function defaultRiskRowFromAmount(amount: number): DraftRiskRow {
|
||||||
return {
|
return {
|
||||||
clientKey: `default-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
clientKey: `default-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
||||||
|
provider_code: "GLOBAL",
|
||||||
draw_id: null,
|
draw_id: null,
|
||||||
normalized_number: "0000",
|
normalized_number: "0000",
|
||||||
cap_amount: amount,
|
cap_amount: amount,
|
||||||
@@ -175,6 +177,7 @@ export function RiskCapDocScreen() {
|
|||||||
setDetail(d);
|
setDetail(d);
|
||||||
const mapped = d.items.map((it) => ({
|
const mapped = d.items.map((it) => ({
|
||||||
clientKey: `srv-${it.id}`,
|
clientKey: `srv-${it.id}`,
|
||||||
|
provider_code: it.provider_code ?? "GLOBAL",
|
||||||
draw_id: it.draw_id,
|
draw_id: it.draw_id,
|
||||||
normalized_number: it.normalized_number,
|
normalized_number: it.normalized_number,
|
||||||
cap_amount: it.cap_amount,
|
cap_amount: it.cap_amount,
|
||||||
@@ -285,6 +288,7 @@ export function RiskCapDocScreen() {
|
|||||||
setSaving(true);
|
setSaving(true);
|
||||||
try {
|
try {
|
||||||
const payload = rowsToSave.map((r) => ({
|
const payload = rowsToSave.map((r) => ({
|
||||||
|
provider_code: r.provider_code ?? "GLOBAL",
|
||||||
draw_id: r.draw_id && r.draw_id > 0 ? r.draw_id : null,
|
draw_id: r.draw_id && r.draw_id > 0 ? r.draw_id : null,
|
||||||
normalized_number: r.normalized_number,
|
normalized_number: r.normalized_number,
|
||||||
cap_amount: r.cap_amount,
|
cap_amount: r.cap_amount,
|
||||||
@@ -294,6 +298,7 @@ export function RiskCapDocScreen() {
|
|||||||
setDetail(d);
|
setDetail(d);
|
||||||
const saved = d.items.map((it) => ({
|
const saved = d.items.map((it) => ({
|
||||||
clientKey: `srv-${it.id}`,
|
clientKey: `srv-${it.id}`,
|
||||||
|
provider_code: it.provider_code ?? "GLOBAL",
|
||||||
draw_id: it.draw_id,
|
draw_id: it.draw_id,
|
||||||
normalized_number: it.normalized_number,
|
normalized_number: it.normalized_number,
|
||||||
cap_amount: it.cap_amount,
|
cap_amount: it.cap_amount,
|
||||||
@@ -320,6 +325,7 @@ export function RiskCapDocScreen() {
|
|||||||
setDetail(d);
|
setDetail(d);
|
||||||
const pub = d.items.map((it) => ({
|
const pub = d.items.map((it) => ({
|
||||||
clientKey: `srv-${it.id}`,
|
clientKey: `srv-${it.id}`,
|
||||||
|
provider_code: it.provider_code ?? "GLOBAL",
|
||||||
draw_id: it.draw_id,
|
draw_id: it.draw_id,
|
||||||
normalized_number: it.normalized_number,
|
normalized_number: it.normalized_number,
|
||||||
cap_amount: it.cap_amount,
|
cap_amount: it.cap_amount,
|
||||||
@@ -351,6 +357,7 @@ export function RiskCapDocScreen() {
|
|||||||
setDetail(d);
|
setDetail(d);
|
||||||
const nd = d.items.map((it) => ({
|
const nd = d.items.map((it) => ({
|
||||||
clientKey: `srv-${it.id}`,
|
clientKey: `srv-${it.id}`,
|
||||||
|
provider_code: it.provider_code ?? "GLOBAL",
|
||||||
draw_id: it.draw_id,
|
draw_id: it.draw_id,
|
||||||
normalized_number: it.normalized_number,
|
normalized_number: it.normalized_number,
|
||||||
cap_amount: it.cap_amount,
|
cap_amount: it.cap_amount,
|
||||||
@@ -424,6 +431,7 @@ export function RiskCapDocScreen() {
|
|||||||
setDetail(d);
|
setDetail(d);
|
||||||
const mapped = d.items.map((it) => ({
|
const mapped = d.items.map((it) => ({
|
||||||
clientKey: `srv-${it.id}`,
|
clientKey: `srv-${it.id}`,
|
||||||
|
provider_code: it.provider_code ?? "GLOBAL",
|
||||||
draw_id: it.draw_id,
|
draw_id: it.draw_id,
|
||||||
normalized_number: it.normalized_number,
|
normalized_number: it.normalized_number,
|
||||||
cap_amount: it.cap_amount,
|
cap_amount: it.cap_amount,
|
||||||
|
|||||||
@@ -100,6 +100,13 @@ export function ReportPreviewTable({
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch (result.key) {
|
switch (result.key) {
|
||||||
|
case "provider_profit":
|
||||||
|
return (
|
||||||
|
<Table>
|
||||||
|
<TableHeader><TableRow><TableHead>开注商</TableHead><TableHead className="text-right">注项数</TableHead><TableHead className="text-right">下注</TableHead><TableHead className="text-right">派彩</TableHead><TableHead className="text-right">平台毛利</TableHead></TableRow></TableHeader>
|
||||||
|
<TableBody>{result.raw.map((item) => <TableRow key={item.provider_code}><TableCell className="font-medium">{item.provider_name}<span className="ml-2 font-mono text-xs text-muted-foreground">{item.provider_code}</span></TableCell><TableCell className="text-right">{item.ticket_item_count}</TableCell><TableCell className="text-right">{formatPlainMoney(item.total_bet_minor, displayCurrency)}</TableCell><TableCell className="text-right">{formatPlainMoney(item.total_payout_minor, displayCurrency)}</TableCell><TableCell className={signedProfitCell(item.approx_house_gross_minor, displayCurrency)}>{formatPlainMoney(item.approx_house_gross_minor, displayCurrency)}</TableCell></TableRow>)}</TableBody>
|
||||||
|
</Table>
|
||||||
|
);
|
||||||
case "draw_profit":
|
case "draw_profit":
|
||||||
return (
|
return (
|
||||||
<Table>
|
<Table>
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { downloadAdminReportJob, postAdminReportJob } from "@/api/admin-report-j
|
|||||||
import {
|
import {
|
||||||
getAdminReportDailyProfit,
|
getAdminReportDailyProfit,
|
||||||
getAdminReportPlayDimension,
|
getAdminReportPlayDimension,
|
||||||
|
getAdminReportProviderProfit,
|
||||||
getAdminReportPlayerWinLoss,
|
getAdminReportPlayerWinLoss,
|
||||||
} from "@/api/admin-reports";
|
} from "@/api/admin-reports";
|
||||||
import {
|
import {
|
||||||
@@ -375,6 +376,23 @@ export function ReportsConsole({ initialCategory = "profit" }: ReportsConsolePro
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "provider_profit": {
|
||||||
|
const payload = await getAdminReportProviderProfit(reportListParams(filters, page, perPage));
|
||||||
|
const currencyCode = resolveDisplayCurrency(payload.currency_code);
|
||||||
|
setDisplayCurrency(currencyCode);
|
||||||
|
const gross = payload.items.reduce((sum, item) => sum + item.approx_house_gross_minor, 0);
|
||||||
|
setResult({
|
||||||
|
key: "provider_profit",
|
||||||
|
raw: payload.items,
|
||||||
|
rows: payload.items,
|
||||||
|
meta: metaFromList(payload.meta),
|
||||||
|
summary: [
|
||||||
|
{ label: t("preview.stats.records"), value: String(payload.meta.total) },
|
||||||
|
{ label: pageScopedLabel("houseGross"), value: formatPlainMoney(gross, currencyCode), tone: gross >= 0 ? "good" : "bad" },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
case "admin_audit": {
|
case "admin_audit": {
|
||||||
const operatorId = filters.operatorId ?? parsePositiveInteger(filters.operator);
|
const operatorId = filters.operatorId ?? parsePositiveInteger(filters.operator);
|
||||||
const payload = await getAdminAuditLogs({
|
const payload = await getAdminAuditLogs({
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { LucideIcon } from "lucide-react";
|
import type { LucideIcon } from "lucide-react";
|
||||||
import {
|
import {
|
||||||
CalendarDays,
|
CalendarDays,
|
||||||
|
Building2,
|
||||||
FileSpreadsheet,
|
FileSpreadsheet,
|
||||||
ListFilter,
|
ListFilter,
|
||||||
ShieldAlert,
|
ShieldAlert,
|
||||||
@@ -44,6 +45,7 @@ export type ReportKey =
|
|||||||
| "player_transfer"
|
| "player_transfer"
|
||||||
| "hot_number_risk"
|
| "hot_number_risk"
|
||||||
| "play_dimension"
|
| "play_dimension"
|
||||||
|
| "provider_profit"
|
||||||
| "sold_out_number"
|
| "sold_out_number"
|
||||||
| "admin_audit";
|
| "admin_audit";
|
||||||
|
|
||||||
@@ -60,6 +62,15 @@ export type ReportDefinition = {
|
|||||||
const PRD_REPORTS_VIEW_ACCESS_ANY = [PRD_REPORT_VIEW] as const;
|
const PRD_REPORTS_VIEW_ACCESS_ANY = [PRD_REPORT_VIEW] as const;
|
||||||
|
|
||||||
export const REPORT_DEFINITIONS: ReportDefinition[] = [
|
export const REPORT_DEFINITIONS: ReportDefinition[] = [
|
||||||
|
{
|
||||||
|
key: "provider_profit",
|
||||||
|
category: "profit",
|
||||||
|
icon: Building2,
|
||||||
|
filterKind: "date",
|
||||||
|
fields: ["period"],
|
||||||
|
connected: true,
|
||||||
|
requiredAny: PRD_REPORTS_VIEW_ACCESS_ANY,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: "draw_profit",
|
key: "draw_profit",
|
||||||
category: "profit",
|
category: "profit",
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import type { AdminTransferOrderItem } from "@/types/api/admin-wallet";
|
|||||||
import type {
|
import type {
|
||||||
AdminReportDailyProfitRow,
|
AdminReportDailyProfitRow,
|
||||||
AdminReportPlayDimensionRow,
|
AdminReportPlayDimensionRow,
|
||||||
|
AdminReportProviderProfitRow,
|
||||||
AdminReportPlayerWinLossRow,
|
AdminReportPlayerWinLossRow,
|
||||||
} from "@/types/api/admin-reports";
|
} from "@/types/api/admin-reports";
|
||||||
|
|
||||||
@@ -59,6 +60,7 @@ export type ReportResult =
|
|||||||
| { key: "player_transfer"; rows: ExportRow[]; summary: StatCard[]; meta: ReportMeta; raw: AdminTransferOrderItem[] }
|
| { key: "player_transfer"; rows: ExportRow[]; summary: StatCard[]; meta: ReportMeta; raw: AdminTransferOrderItem[] }
|
||||||
| { key: "hot_number_risk"; rows: ExportRow[]; summary: StatCard[]; meta: ReportMeta | null; raw: AdminRiskPoolShowData }
|
| { key: "hot_number_risk"; rows: ExportRow[]; summary: StatCard[]; meta: ReportMeta | null; raw: AdminRiskPoolShowData }
|
||||||
| { key: "play_dimension"; rows: ExportRow[]; summary: StatCard[]; meta: ReportMeta; raw: AdminReportPlayDimensionRow[] }
|
| { key: "play_dimension"; rows: ExportRow[]; summary: StatCard[]; meta: ReportMeta; raw: AdminReportPlayDimensionRow[] }
|
||||||
|
| { key: "provider_profit"; rows: ExportRow[]; summary: StatCard[]; meta: ReportMeta; raw: AdminReportProviderProfitRow[] }
|
||||||
| { key: "sold_out_number"; rows: ExportRow[]; summary: StatCard[]; meta: ReportMeta; raw: AdminRiskPoolRow[] }
|
| { key: "sold_out_number"; rows: ExportRow[]; summary: StatCard[]; meta: ReportMeta; raw: AdminRiskPoolRow[] }
|
||||||
| { key: "admin_audit"; rows: ExportRow[]; summary: StatCard[]; meta: ReportMeta; raw: AdminAuditLogRow[] };
|
| { key: "admin_audit"; rows: ExportRow[]; summary: StatCard[]; meta: ReportMeta; raw: AdminAuditLogRow[] };
|
||||||
|
|
||||||
|
|||||||
@@ -33,9 +33,11 @@ import type { AdminRiskPoolDetailLogRow, AdminRiskPoolShowData } from "@/types/a
|
|||||||
export function RiskPoolDetailConsole({
|
export function RiskPoolDetailConsole({
|
||||||
drawId,
|
drawId,
|
||||||
number4d,
|
number4d,
|
||||||
|
providerCode,
|
||||||
}: {
|
}: {
|
||||||
drawId: number;
|
drawId: number;
|
||||||
number4d: string;
|
number4d: string;
|
||||||
|
providerCode: string;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation(["risk", "common"]);
|
const { t } = useTranslation(["risk", "common"]);
|
||||||
const tRef = useTranslationRef(["risk", "common"]);
|
const tRef = useTranslationRef(["risk", "common"]);
|
||||||
@@ -53,7 +55,11 @@ export function RiskPoolDetailConsole({
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
const d = await getAdminRiskPoolDetail(drawId, number4d, { page, per_page: perPage });
|
const d = await getAdminRiskPoolDetail(drawId, number4d, {
|
||||||
|
page,
|
||||||
|
per_page: perPage,
|
||||||
|
provider_code: providerCode,
|
||||||
|
});
|
||||||
setData(d);
|
setData(d);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const msg =
|
const msg =
|
||||||
@@ -63,11 +69,11 @@ export function RiskPoolDetailConsole({
|
|||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [drawId, number4d, page, perPage, tRef]);
|
}, [drawId, number4d, page, perPage, providerCode, tRef]);
|
||||||
|
|
||||||
useAsyncEffect(() => {
|
useAsyncEffect(() => {
|
||||||
void load();
|
void load();
|
||||||
}, [drawId, number4d, page, perPage]);
|
}, [drawId, number4d, page, perPage, providerCode]);
|
||||||
|
|
||||||
if (error && !data) {
|
if (error && !data) {
|
||||||
return (
|
return (
|
||||||
@@ -115,7 +121,9 @@ export function RiskPoolDetailConsole({
|
|||||||
<CardTitle className="text-lg">
|
<CardTitle className="text-lg">
|
||||||
{t("numberTitle", { number: pool.normalized_number })}
|
{t("numberTitle", { number: pool.normalized_number })}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<p className="text-sm text-muted-foreground">{t("drawMeta", { drawNo: data.draw_no })}</p>
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{t("drawMeta", { drawNo: data.draw_no })} · {t("provider")} {pool.provider_code}
|
||||||
|
</p>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
<CardContent className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
||||||
<div className="rounded-lg border bg-muted/40 p-3">
|
<div className="rounded-lg border bg-muted/40 p-3">
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ export function RiskPoolsConsole({
|
|||||||
const [perPage, setPerPage] = useState(10);
|
const [perPage, setPerPage] = useState(10);
|
||||||
const [data, setData] = useState<AdminRiskPoolListData | null>(null);
|
const [data, setData] = useState<AdminRiskPoolListData | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [actingNumber, setActingNumber] = useState<string | null>(null);
|
const [actingKey, setActingKey] = useState<string | null>(null);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
const load = useCallback(async () => {
|
const load = useCallback(async () => {
|
||||||
@@ -152,11 +152,12 @@ export function RiskPoolsConsole({
|
|||||||
|
|
||||||
const handleManualStatus = useCallback(
|
const handleManualStatus = useCallback(
|
||||||
async (row: AdminRiskPoolRow) => {
|
async (row: AdminRiskPoolRow) => {
|
||||||
setActingNumber(row.normalized_number);
|
const rowKey = `${row.provider_code}:${row.normalized_number}`;
|
||||||
|
setActingKey(rowKey);
|
||||||
try {
|
try {
|
||||||
const updated = row.is_sold_out
|
const updated = row.is_sold_out
|
||||||
? await postAdminRiskPoolRecover(drawId, row.normalized_number)
|
? await postAdminRiskPoolRecover(drawId, row.normalized_number, row.provider_code)
|
||||||
: await postAdminRiskPoolManualClose(drawId, row.normalized_number);
|
: await postAdminRiskPoolManualClose(drawId, row.normalized_number, row.provider_code);
|
||||||
|
|
||||||
setData((current) => {
|
setData((current) => {
|
||||||
if (!current) return current;
|
if (!current) return current;
|
||||||
@@ -164,7 +165,10 @@ export function RiskPoolsConsole({
|
|||||||
return {
|
return {
|
||||||
...current,
|
...current,
|
||||||
items: current.items.map((item) =>
|
items: current.items.map((item) =>
|
||||||
item.normalized_number === updated.normalized_number ? updated : item,
|
item.normalized_number === updated.normalized_number &&
|
||||||
|
item.provider_code === updated.provider_code
|
||||||
|
? updated
|
||||||
|
: item,
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -172,7 +176,7 @@ export function RiskPoolsConsole({
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast.error(e instanceof LotteryApiBizError ? e.message : t("actionFailed"));
|
toast.error(e instanceof LotteryApiBizError ? e.message : t("actionFailed"));
|
||||||
} finally {
|
} finally {
|
||||||
setActingNumber(null);
|
setActingKey(null);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[drawId, t],
|
[drawId, t],
|
||||||
@@ -275,6 +279,7 @@ export function RiskPoolsConsole({
|
|||||||
<Table id={`risk-pools-table-${drawId}`}>
|
<Table id={`risk-pools-table-${drawId}`}>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
|
<TableHead>{t("provider")}</TableHead>
|
||||||
<TableHead>{t("searchNumber")}</TableHead>
|
<TableHead>{t("searchNumber")}</TableHead>
|
||||||
<TableHead className="text-center">{t("capAmount")}</TableHead>
|
<TableHead className="text-center">{t("capAmount")}</TableHead>
|
||||||
<TableHead className="text-center">{t("lockedAmount")}</TableHead>
|
<TableHead className="text-center">{t("lockedAmount")}</TableHead>
|
||||||
@@ -285,15 +290,15 @@ export function RiskPoolsConsole({
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{loading && !data ? <AdminTableLoadingRow colSpan={7} /> : null}
|
{loading && !data ? <AdminTableLoadingRow colSpan={8} /> : null}
|
||||||
{(data?.items ?? []).map((row: AdminRiskPoolRow) => {
|
{(data?.items ?? []).map((row: AdminRiskPoolRow) => {
|
||||||
const highRisk = (row.usage_ratio ?? 0) >= 0.8;
|
const highRisk = (row.usage_ratio ?? 0) >= 0.8;
|
||||||
const acting = actingNumber === row.normalized_number;
|
const acting = actingKey === `${row.provider_code}:${row.normalized_number}`;
|
||||||
const currencyCode = data?.currency_code ?? "NPR";
|
const currencyCode = data?.currency_code ?? "NPR";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRow
|
<TableRow
|
||||||
key={row.normalized_number}
|
key={`${row.provider_code}-${row.normalized_number}`}
|
||||||
className={cn(
|
className={cn(
|
||||||
row.is_sold_out
|
row.is_sold_out
|
||||||
? "bg-red-50/90 hover:bg-red-50 dark:bg-red-950/25 dark:hover:bg-red-950/35"
|
? "bg-red-50/90 hover:bg-red-50 dark:bg-red-950/25 dark:hover:bg-red-950/35"
|
||||||
@@ -302,6 +307,7 @@ export function RiskPoolsConsole({
|
|||||||
: null,
|
: null,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
<TableCell className="font-mono text-xs">{row.provider_code}</TableCell>
|
||||||
<TableCell className="font-mono font-medium">{row.normalized_number}</TableCell>
|
<TableCell className="font-mono font-medium">{row.normalized_number}</TableCell>
|
||||||
<TableCell className="text-center text-sm font-semibold">
|
<TableCell className="text-center text-sm font-semibold">
|
||||||
{formatAdminMinorUnits(row.total_cap_amount, currencyCode)}
|
{formatAdminMinorUnits(row.total_cap_amount, currencyCode)}
|
||||||
@@ -331,7 +337,7 @@ export function RiskPoolsConsole({
|
|||||||
key: "view",
|
key: "view",
|
||||||
label: t("view"),
|
label: t("view"),
|
||||||
icon: Eye,
|
icon: Eye,
|
||||||
href: `/admin/draws/${drawId}/risk/pools/${row.normalized_number}`,
|
href: `/admin/draws/${drawId}/risk/pools/${row.normalized_number}?provider_code=${encodeURIComponent(row.provider_code)}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "toggle",
|
key: "toggle",
|
||||||
@@ -347,9 +353,11 @@ export function RiskPoolsConsole({
|
|||||||
description: row.is_sold_out
|
description: row.is_sold_out
|
||||||
? t("confirm.recoverDescription", {
|
? t("confirm.recoverDescription", {
|
||||||
number: row.normalized_number,
|
number: row.normalized_number,
|
||||||
|
provider: row.provider_code,
|
||||||
})
|
})
|
||||||
: t("confirm.closeDescription", {
|
: t("confirm.closeDescription", {
|
||||||
number: row.normalized_number,
|
number: row.normalized_number,
|
||||||
|
provider: row.provider_code,
|
||||||
}),
|
}),
|
||||||
confirmVariant: row.is_sold_out ? "default" : "destructive",
|
confirmVariant: row.is_sold_out ? "default" : "destructive",
|
||||||
onConfirm: () => handleManualStatus(row),
|
onConfirm: () => handleManualStatus(row),
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ export type OddsVersionDetail = ConfigVersionSummary & {
|
|||||||
|
|
||||||
export type RiskCapItemRow = {
|
export type RiskCapItemRow = {
|
||||||
id: number;
|
id: number;
|
||||||
|
provider_code?: string;
|
||||||
draw_id: number | null;
|
draw_id: number | null;
|
||||||
normalized_number: string;
|
normalized_number: string;
|
||||||
cap_amount: number;
|
cap_amount: number;
|
||||||
|
|||||||
@@ -24,6 +24,15 @@ export type AdminReportPlayDimensionRow = {
|
|||||||
approx_house_gross_minor: number;
|
approx_house_gross_minor: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type AdminReportProviderProfitRow = {
|
||||||
|
provider_code: string;
|
||||||
|
provider_name: string;
|
||||||
|
ticket_item_count: number;
|
||||||
|
total_bet_minor: number;
|
||||||
|
total_payout_minor: number;
|
||||||
|
approx_house_gross_minor: number;
|
||||||
|
};
|
||||||
|
|
||||||
export type AdminReportListData<T> = {
|
export type AdminReportListData<T> = {
|
||||||
items: T[];
|
items: T[];
|
||||||
meta: {
|
meta: {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export type AdminRiskPoolListMeta = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type AdminRiskPoolRow = {
|
export type AdminRiskPoolRow = {
|
||||||
|
provider_code: string;
|
||||||
normalized_number: string;
|
normalized_number: string;
|
||||||
total_cap_amount: number;
|
total_cap_amount: number;
|
||||||
locked_amount: number;
|
locked_amount: number;
|
||||||
@@ -26,6 +27,7 @@ export type AdminRiskPoolListData = {
|
|||||||
|
|
||||||
export type AdminRiskPoolDetailLogRow = {
|
export type AdminRiskPoolDetailLogRow = {
|
||||||
id: number;
|
id: number;
|
||||||
|
provider_code: string;
|
||||||
action_type: string;
|
action_type: string;
|
||||||
amount: number;
|
amount: number;
|
||||||
source_reason: string | null;
|
source_reason: string | null;
|
||||||
|
|||||||
Reference in New Issue
Block a user