退款时压缩卖方加价,人工/自动批次仅完结单可入账,号主收入进主站钱包;仲裁后重算释放并支持作废未入账 DS。开通时四种模式按默认生效,对外口径合并为 35/40/25,不再单独展示通道费。 Co-authored-by: Cursor <cursoragent@cursor.com>
624 lines
16 KiB
TypeScript
624 lines
16 KiB
TypeScript
import axios from 'axios'
|
|
|
|
const TOKEN_KEY = 'hfb_dist_access_token'
|
|
|
|
export function getStoredToken(): string | null {
|
|
try {
|
|
return localStorage.getItem(TOKEN_KEY)
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
export function setStoredToken(token: string): void {
|
|
try {
|
|
localStorage.setItem(TOKEN_KEY, token)
|
|
} catch {
|
|
// ignore local storage errors
|
|
}
|
|
}
|
|
|
|
export function clearStoredToken(): void {
|
|
try {
|
|
localStorage.removeItem(TOKEN_KEY)
|
|
} catch {
|
|
// ignore local storage errors
|
|
}
|
|
}
|
|
|
|
const api = axios.create({ baseURL: '/api', withCredentials: true })
|
|
|
|
api.interceptors.request.use((config) => {
|
|
const token = getStoredToken()
|
|
if (token) {
|
|
config.headers.Authorization = `Bearer ${token}`
|
|
}
|
|
return config
|
|
})
|
|
|
|
api.interceptors.response.use(
|
|
(resp) => resp,
|
|
(error) => {
|
|
if (error?.response?.status === 401) {
|
|
clearStoredToken()
|
|
if (typeof window !== 'undefined' && !window.location.pathname.startsWith('/login')) {
|
|
window.location.href = '/login'
|
|
}
|
|
}
|
|
return Promise.reject(error)
|
|
},
|
|
)
|
|
|
|
export interface ApiResponse<T> {
|
|
data: T
|
|
code?: string | number
|
|
message?: string
|
|
}
|
|
|
|
export function extract<T>(resp: { data: ApiResponse<T> }): T {
|
|
return resp.data.data
|
|
}
|
|
|
|
export function formatMoney(cent: number | undefined | null): string {
|
|
return `¥${((cent ?? 0) / 100).toFixed(2)}`
|
|
}
|
|
|
|
export interface CurrentMember {
|
|
admin_id: number
|
|
username: string
|
|
nickname: string
|
|
is_superadmin: boolean
|
|
site_id?: number
|
|
site_code?: string
|
|
site_name?: string
|
|
domain?: string
|
|
member_type?: string
|
|
}
|
|
|
|
export interface LoginResult {
|
|
token: string
|
|
refresh_token?: string
|
|
member: CurrentMember
|
|
}
|
|
|
|
export async function login(username: string, password: string): Promise<LoginResult> {
|
|
const resp = await api.post<ApiResponse<LoginResult>>('/distribution/auth/login', {
|
|
username,
|
|
password,
|
|
})
|
|
const res = extract(resp)
|
|
if (res.token) {
|
|
setStoredToken(res.token)
|
|
}
|
|
return res
|
|
}
|
|
|
|
export async function getMe(): Promise<CurrentMember> {
|
|
const resp = await api.get<ApiResponse<CurrentMember>>('/distribution/auth/me')
|
|
return extract(resp)
|
|
}
|
|
|
|
export async function logout(): Promise<void> {
|
|
try {
|
|
await api.post('/distribution/auth/logout')
|
|
} finally {
|
|
clearStoredToken()
|
|
}
|
|
}
|
|
|
|
export interface Site {
|
|
id: number
|
|
site_code: string
|
|
site_name: string
|
|
domain: string
|
|
status: string
|
|
display_account_enabled: boolean
|
|
tax_enabled: boolean
|
|
tax_rate_bps: number
|
|
settlement_hold_days?: number
|
|
logo_url?: string
|
|
favicon_url?: string
|
|
banners_json?: string
|
|
announcements_json?: string
|
|
footer_info_json?: string
|
|
created_at: string
|
|
}
|
|
|
|
export interface Rule {
|
|
id: number
|
|
site_id?: number
|
|
mode: string
|
|
version: number
|
|
status: string
|
|
platform_receive_ratio: number
|
|
platform_sell_ratio: number
|
|
distributor_receive_ratio: number
|
|
distributor_sell_ratio: number
|
|
distributor_a_ratio: number
|
|
distributor_b_ratio: number
|
|
service_fee_value: number
|
|
service_fee_unit: string
|
|
payment_fee_bps: number
|
|
tax_enabled: boolean
|
|
tax_rate_bps: number
|
|
effective_from: string
|
|
effective_to?: string
|
|
}
|
|
|
|
export interface DistributionOrder {
|
|
order_id: number
|
|
order_no: string
|
|
site_id: number
|
|
status: string
|
|
handoff_status?: string
|
|
refund_status: string
|
|
settlement_status: string
|
|
distributor_settlement_status?: string
|
|
release_at?: string
|
|
distribution_mode: string
|
|
gross_paid_amount_cent?: number
|
|
refund_amount_cent?: number
|
|
net_paid_amount_cent: number
|
|
payment_fee_cent: number
|
|
service_fee_cent: number
|
|
markup_amount_cent: number
|
|
tax_amount_cent: number
|
|
split_base_amount_cent: number
|
|
rounding_adjustment_cent: number
|
|
created_at: string
|
|
|
|
// 商品与租客字段
|
|
listing_no?: string
|
|
title?: string
|
|
game_name?: string
|
|
server_region?: string
|
|
login_platform?: string
|
|
rent_duration_hours?: number
|
|
rented_at?: string
|
|
renter_phone?: string
|
|
renter_nickname?: string
|
|
owner_rent_amount_cent?: number
|
|
base_price_cent?: number
|
|
shelf_markup_cent?: number
|
|
deposit_amount_cent?: number
|
|
|
|
// 商户本站收益拆解字段
|
|
distributor_earning_cent?: number
|
|
distributor_role?: string
|
|
distributor_markup_cent?: number
|
|
distributor_split_cent?: number
|
|
}
|
|
|
|
export interface OrderPage {
|
|
items: DistributionOrder[]
|
|
total: number
|
|
page: number
|
|
page_size: number
|
|
}
|
|
|
|
export interface AccountDisplay {
|
|
id: number
|
|
site_id: number
|
|
account_id: number
|
|
listing_id: number
|
|
display_status: string
|
|
markup_amount_cent?: number
|
|
shelf_ratio_delta?: number
|
|
base_ratio?: number
|
|
site_ratio?: number
|
|
listing_no?: string
|
|
title?: string
|
|
game_name?: string
|
|
server_region?: string
|
|
login_platform?: string
|
|
rank_level?: string
|
|
haf_coin_amount?: number
|
|
price_cent?: number
|
|
site_price_cent?: number
|
|
deposit_amount_cent?: number
|
|
listing_status?: string
|
|
in_transaction?: boolean
|
|
listing_site_id?: number | null
|
|
source_channel?: string
|
|
}
|
|
|
|
export interface SyncSiteAccountsResult {
|
|
synced_count: number
|
|
skipped_count: number
|
|
total_active: number
|
|
}
|
|
|
|
export interface PublicDistributionListing {
|
|
listing_id: number
|
|
listing_no: string
|
|
account_id: number
|
|
title: string
|
|
game_name: string
|
|
server_region: string
|
|
login_platform: string
|
|
rank_level: string
|
|
haf_coin_amount: number
|
|
price_cent: number
|
|
deposit_amount_cent: number
|
|
in_transaction: boolean
|
|
site_code: string
|
|
site_name: string
|
|
}
|
|
|
|
export interface PublicDistributionListingsResult {
|
|
items: PublicDistributionListing[]
|
|
total: number
|
|
page: number
|
|
page_size: number
|
|
}
|
|
|
|
export async function fetchPublicSiteListings(
|
|
siteCode: string,
|
|
page = 1,
|
|
pageSize = 24,
|
|
): Promise<PublicDistributionListingsResult> {
|
|
const resp = await api.get<ApiResponse<PublicDistributionListingsResult>>(
|
|
`/distribution/sites/${siteCode}/listings`,
|
|
{
|
|
params: { page, page_size: pageSize },
|
|
},
|
|
)
|
|
return extract(resp)
|
|
}
|
|
|
|
export interface PaymentChannel {
|
|
id: number
|
|
site_id: number
|
|
config_id: number
|
|
priority: number
|
|
status: string
|
|
}
|
|
|
|
export interface SettlementBatch {
|
|
id: number
|
|
site_id: number
|
|
batch_no: string
|
|
period_start: string
|
|
period_end: string
|
|
total_amount_cent: number
|
|
status: string
|
|
}
|
|
|
|
export interface SettlementItem {
|
|
id: number
|
|
order_id: number
|
|
party_role: string
|
|
amount_cent: number
|
|
status: string
|
|
}
|
|
|
|
export interface OnboardSitePayload {
|
|
site_code: string
|
|
site_name: string
|
|
domain: string
|
|
admin_username: string
|
|
admin_password: string
|
|
admin_nickname?: string
|
|
member_type?: string
|
|
display_account_enabled?: boolean
|
|
tax_enabled?: boolean
|
|
tax_rate_bps?: number
|
|
settlement_hold_days?: number
|
|
}
|
|
|
|
export interface OnboardSiteResult {
|
|
site: Site
|
|
rule: Rule
|
|
member: CurrentMember
|
|
}
|
|
|
|
export async function onboardSite(payload: OnboardSitePayload): Promise<OnboardSiteResult> {
|
|
const resp = await api.post<ApiResponse<OnboardSiteResult>>('/admin/distribution/sites/onboard', payload)
|
|
return extract(resp)
|
|
}
|
|
|
|
export async function listSites(): Promise<Site[]> {
|
|
const resp = await api.get<ApiResponse<{ items: Site[] }>>('/admin/distribution/sites')
|
|
return extract(resp).items ?? []
|
|
}
|
|
|
|
export async function listRules(siteID?: number, mode?: string): Promise<Rule[]> {
|
|
const resp = await api.get<ApiResponse<{ items: Rule[] }>>('/admin/distribution/rules', {
|
|
params: { site_id: siteID || undefined, mode: mode || undefined },
|
|
})
|
|
return extract(resp).items ?? []
|
|
}
|
|
|
|
export interface CreateRulePayload {
|
|
site_id?: number
|
|
mode: string
|
|
distributor_receive_ratio?: number
|
|
platform_sell_ratio?: number
|
|
distributor_sell_ratio?: number
|
|
platform_receive_ratio?: number
|
|
distributor_a_ratio?: number
|
|
distributor_b_ratio?: number
|
|
service_fee_value: number
|
|
service_fee_unit: string
|
|
payment_fee_bps: number
|
|
tax_enabled: boolean
|
|
tax_rate_bps: number
|
|
effective_from?: string
|
|
}
|
|
|
|
export async function createRule(payload: CreateRulePayload): Promise<Rule> {
|
|
const resp = await api.post<ApiResponse<Rule>>('/admin/distribution/rules', payload)
|
|
return extract(resp)
|
|
}
|
|
|
|
export async function updateRuleStatus(id: number, status: string): Promise<Rule> {
|
|
const resp = await api.put<ApiResponse<Rule>>(`/admin/distribution/rules/${id}/status`, { status })
|
|
return extract(resp)
|
|
}
|
|
|
|
export async function listOrders(params: {
|
|
kind?: string
|
|
role?: string
|
|
status?: string
|
|
siteID?: number
|
|
page?: number
|
|
pageSize?: number
|
|
keyword?: string
|
|
}): Promise<OrderPage> {
|
|
const resp = await api.get<ApiResponse<OrderPage>>('/admin/distribution/orders', {
|
|
params: {
|
|
kind: params.kind || undefined,
|
|
role: params.role || undefined,
|
|
status: params.status || undefined,
|
|
keyword: params.keyword || undefined,
|
|
site_id: params.siteID || undefined,
|
|
page: params.page || 1,
|
|
page_size: params.pageSize || 20,
|
|
},
|
|
})
|
|
return extract(resp)
|
|
}
|
|
|
|
export async function listAccountDisplays(siteID: number, status?: string): Promise<AccountDisplay[]> {
|
|
const resp = await api.get<ApiResponse<{ items: AccountDisplay[] }>>('/admin/distribution/accounts', {
|
|
params: { site_id: siteID, status: status || undefined },
|
|
})
|
|
return extract(resp).items ?? []
|
|
}
|
|
|
|
export async function updateAccountDisplay(id: number, displayStatus: string): Promise<AccountDisplay> {
|
|
const resp = await api.put<ApiResponse<AccountDisplay>>(`/admin/distribution/accounts/${id}/display`, {
|
|
display_status: displayStatus,
|
|
})
|
|
return extract(resp)
|
|
}
|
|
|
|
export async function syncSiteAccounts(siteID: number): Promise<SyncSiteAccountsResult> {
|
|
const resp = await api.post<ApiResponse<SyncSiteAccountsResult>>(`/admin/distribution/sites/${siteID}/sync-accounts`)
|
|
return extract(resp)
|
|
}
|
|
|
|
export async function listPaymentChannels(siteID: number): Promise<PaymentChannel[]> {
|
|
const resp = await api.get<ApiResponse<{ items: PaymentChannel[] }>>('/admin/distribution/payment-channels', {
|
|
params: { site_id: siteID },
|
|
})
|
|
return extract(resp).items ?? []
|
|
}
|
|
|
|
export async function bindPaymentChannel(siteID: number, configID: number, priority: number): Promise<PaymentChannel> {
|
|
const resp = await api.post<ApiResponse<PaymentChannel>>(
|
|
'/admin/distribution/payment-channels',
|
|
{ config_id: configID, priority },
|
|
{ params: { site_id: siteID } },
|
|
)
|
|
return extract(resp)
|
|
}
|
|
|
|
export async function unbindPaymentChannel(id: number): Promise<void> {
|
|
await api.delete(`/admin/distribution/payment-channels/${id}`)
|
|
}
|
|
|
|
export async function listSettlementBatches(siteID: number): Promise<SettlementBatch[]> {
|
|
const resp = await api.get<ApiResponse<{ items: SettlementBatch[] }>>('/admin/distribution/settlement-batches', {
|
|
params: { site_id: siteID },
|
|
})
|
|
return extract(resp).items ?? []
|
|
}
|
|
|
|
export async function createSettlementBatch(siteID: number, periodStart: string, periodEnd: string): Promise<SettlementBatch> {
|
|
const resp = await api.post<ApiResponse<SettlementBatch>>('/admin/distribution/settlement-batches', {
|
|
site_id: siteID,
|
|
period_start: periodStart,
|
|
period_end: periodEnd,
|
|
})
|
|
return extract(resp)
|
|
}
|
|
|
|
export async function settleBatch(id: number): Promise<SettlementBatch> {
|
|
const resp = await api.post<ApiResponse<SettlementBatch>>(`/admin/distribution/settlement-batches/${id}/settle`)
|
|
return extract(resp)
|
|
}
|
|
|
|
export async function cancelSettlementBatch(id: number): Promise<SettlementBatch> {
|
|
const resp = await api.post<ApiResponse<SettlementBatch>>(`/admin/distribution/settlement-batches/${id}/cancel`)
|
|
return extract(resp)
|
|
}
|
|
|
|
export async function listSettlementItems(batchID: number): Promise<SettlementItem[]> {
|
|
const resp = await api.get<ApiResponse<{ items: SettlementItem[] }>>(`/admin/distribution/settlement-batches/${batchID}/items`)
|
|
return extract(resp).items ?? []
|
|
}
|
|
|
|
export interface ReceivedListing {
|
|
listing_id: number
|
|
listing_no: string
|
|
site_id: number
|
|
account_id: number
|
|
owner_id: number
|
|
owner_phone?: string
|
|
owner_nickname?: string
|
|
title: string
|
|
game_name: string
|
|
server_region: string
|
|
login_platform: string
|
|
rank_level: string
|
|
haf_coin_amount: number
|
|
price_cent: number
|
|
deposit_amount_cent: number
|
|
status: string
|
|
review_status: string
|
|
review_reason: string
|
|
published_at?: string
|
|
created_at: string
|
|
}
|
|
|
|
export interface ReceivedListingPage {
|
|
items: ReceivedListing[]
|
|
total: number
|
|
page: number
|
|
page_size: number
|
|
}
|
|
|
|
export async function fetchReceivedListings(params: {
|
|
siteID?: number
|
|
page?: number
|
|
pageSize?: number
|
|
status?: string
|
|
reviewStatus?: string
|
|
gameName?: string
|
|
keyword?: string
|
|
}): Promise<ReceivedListingPage> {
|
|
const resp = await api.get<ApiResponse<ReceivedListingPage>>('/admin/distribution/received-listings', {
|
|
params: {
|
|
site_id: params.siteID,
|
|
page: params.page || 1,
|
|
page_size: params.pageSize || 20,
|
|
status: params.status || undefined,
|
|
review_status: params.reviewStatus || undefined,
|
|
game_name: params.gameName || undefined,
|
|
keyword: params.keyword || undefined,
|
|
},
|
|
})
|
|
return extract(resp)
|
|
}
|
|
|
|
export async function updateReceivedListingStatus(id: number, status: string): Promise<void> {
|
|
await api.put(`/admin/distribution/received-listings/${id}/status`, { status })
|
|
}
|
|
|
|
export interface SiteWallet {
|
|
site_id: number
|
|
available_balance_cent: number
|
|
frozen_balance_cent: number
|
|
pending_release_cent: number
|
|
total_settled_cent: number
|
|
total_withdrawn_cent: number
|
|
settlement_hold_days?: number
|
|
}
|
|
|
|
export async function fetchSiteWallet(siteID: number): Promise<SiteWallet> {
|
|
const resp = await api.get<ApiResponse<SiteWallet>>(`/admin/distribution/sites/${siteID}/wallet`)
|
|
return extract(resp)
|
|
}
|
|
|
|
export interface SiteWithdrawal {
|
|
id: number
|
|
withdraw_no: string
|
|
site_id: number
|
|
amount_cent: number
|
|
fee_cent: number
|
|
actual_amount_cent: number
|
|
account_type: string
|
|
account_name: string
|
|
account_no: string
|
|
bank_name?: string
|
|
bank_branch?: string
|
|
status: string
|
|
review_remark?: string
|
|
payment_proof_url?: string
|
|
payment_remark?: string
|
|
paid_at?: string
|
|
created_at: string
|
|
}
|
|
|
|
export interface SiteWithdrawalPage {
|
|
items: SiteWithdrawal[]
|
|
total: number
|
|
page: number
|
|
page_size: number
|
|
}
|
|
|
|
export async function fetchSiteWithdrawals(siteID: number, page = 1, pageSize = 20): Promise<SiteWithdrawalPage> {
|
|
const resp = await api.get<ApiResponse<SiteWithdrawalPage>>(`/admin/distribution/sites/${siteID}/withdrawals`, {
|
|
params: { page, page_size: pageSize },
|
|
})
|
|
return extract(resp)
|
|
}
|
|
|
|
export interface ApplySiteWithdrawalPayload {
|
|
site_id?: number
|
|
amount_cent: number
|
|
account_type: 'alipay' | 'bank'
|
|
account_name: string
|
|
account_no: string
|
|
bank_name?: string
|
|
bank_branch?: string
|
|
}
|
|
|
|
export async function applySiteWithdrawal(payload: ApplySiteWithdrawalPayload): Promise<SiteWithdrawal> {
|
|
const resp = await api.post<ApiResponse<SiteWithdrawal>>(`/admin/distribution/sites/${payload.site_id}/withdrawals`, payload)
|
|
return extract(resp)
|
|
}
|
|
|
|
export interface SiteBrandingPayload {
|
|
logo_url?: string
|
|
favicon_url?: string
|
|
banners_json?: string
|
|
announcements_json?: string
|
|
footer_info_json?: string
|
|
}
|
|
|
|
export async function updateSiteBranding(siteID: number, payload: SiteBrandingPayload): Promise<Site> {
|
|
const resp = await api.put<ApiResponse<Site>>(`/admin/distribution/sites/${siteID}/branding`, payload)
|
|
return extract(resp)
|
|
}
|
|
|
|
export async function updateAccountMarkup(id: number, shelfRatioDelta: number): Promise<AccountDisplay> {
|
|
const resp = await api.put<ApiResponse<AccountDisplay>>(`/admin/distribution/accounts/${id}/markup`, {
|
|
shelf_ratio_delta: shelfRatioDelta,
|
|
})
|
|
return extract(resp)
|
|
}
|
|
|
|
export async function batchUpdateAccountMarkup(siteID: number, listingIDs: number[], shelfRatioDelta: number): Promise<void> {
|
|
await api.post(`/admin/distribution/accounts/batch-markup`, {
|
|
listing_ids: listingIDs,
|
|
shelf_ratio_delta: shelfRatioDelta,
|
|
}, {
|
|
params: { site_id: siteID },
|
|
})
|
|
}
|
|
|
|
export interface DistributionAnalytics {
|
|
today_received_count: number
|
|
total_received_count: number
|
|
active_listing_count: number
|
|
today_order_count: number
|
|
today_gmv_cent: number
|
|
today_profit_cent: number
|
|
month_profit_cent: number
|
|
available_balance_cent: number
|
|
trend_days: {
|
|
date: string
|
|
received_count: number
|
|
order_count: number
|
|
gmv_cent: number
|
|
profit_cent: number
|
|
}[]
|
|
}
|
|
|
|
export async function fetchDashboardAnalytics(siteID: number): Promise<DistributionAnalytics> {
|
|
const resp = await api.get<ApiResponse<DistributionAnalytics>>(`/admin/distribution/sites/${siteID}/analytics`)
|
|
return extract(resp)
|
|
}
|