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 { data: T code?: string | number message?: string } export function extract(resp: { data: ApiResponse }): T { return resp.data.data } export function formatMoney(cent: number | undefined | null): string { return `¥${((cent ?? 0) / 100).toFixed(2)}` } export function formatRatio(ratio: number | undefined | null): string { const value = Number(ratio || 0) if (!Number.isFinite(value) || value <= 0) return '--' return `1:${value.toFixed(1)}` } 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 { const resp = await api.post>('/distribution/auth/login', { username, password, }) const res = extract(resp) if (res.token) { setStoredToken(res.token) } return res } export async function getMe(): Promise { const resp = await api.get>('/distribution/auth/me') return extract(resp) } export async function logout(): Promise { 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 aw_recycle_json?: string cooperation_feedback_json?: string storefront_text_json?: string listing_qrcode_url?: string receive_ratio_delta?: number receive_cost_delta?: number admin_user_id?: number admin_username?: string admin_nickname?: string member_type?: 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 SyncSiteAccountsRequest { listing_ids?: number[] clean_stale_listings?: boolean default_shelf_ratio_delta?: number } export interface SyncSiteAccountsResult { synced_count: number skipped_count: number updated_count?: number reenabled_count?: number disabled_stale_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 { const resp = await api.get>( `/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 { const resp = await api.post>('/admin/distribution/sites/onboard', payload) return extract(resp) } export async function listSites(): Promise { const resp = await api.get>('/admin/distribution/sites') return extract(resp).items ?? [] } export async function updateSite( siteID: number, payload: Partial< Pick< Site, | 'site_name' | 'domain' | 'status' | 'display_account_enabled' | 'tax_enabled' | 'tax_rate_bps' | 'settlement_hold_days' | 'listing_qrcode_url' | 'receive_ratio_delta' | 'receive_cost_delta' > > ): Promise { const resp = await api.put>(`/admin/distribution/sites/${siteID}`, payload) return extract(resp) } export async function deleteSite(siteID: number): Promise<{ deleted: boolean }> { const resp = await api.delete>(`/admin/distribution/sites/${siteID}`) return extract(resp) } export async function resetSiteAdminPassword(siteID: number, newPassword: string): Promise<{ message: string }> { const resp = await api.put>(`/admin/distribution/sites/${siteID}/admin-password`, { new_password: newPassword, }) return extract(resp) } export interface DistributionSiteDefaults { default_settlement_hold_days: number } // fetchDistributionSiteDefaults 读取新建站点的默认结算冷却天数,供开通表单预填。 export async function fetchDistributionSiteDefaults(): Promise { const resp = await api.get>( '/admin/distribution/site-defaults' ) return extract(resp) } export async function listRules(siteID?: number, mode?: string): Promise { const resp = await api.get>('/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 { const resp = await api.post>('/admin/distribution/rules', payload) return extract(resp) } export async function updateRuleStatus(id: number, status: string): Promise { const resp = await api.put>(`/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 { const resp = await api.get>('/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 { const resp = await api.get>('/admin/distribution/accounts', { params: { site_id: siteID, status: status || undefined }, }) return extract(resp).items ?? [] } export async function updateAccountDisplay(id: number, displayStatus: string): Promise { const resp = await api.put>(`/admin/distribution/accounts/${id}/display`, { display_status: displayStatus, }) return extract(resp) } export async function syncSiteAccounts(siteID: number, req?: SyncSiteAccountsRequest): Promise { const resp = await api.post>(`/admin/distribution/sites/${siteID}/sync-accounts`, req ?? {}) return extract(resp) } export async function listPaymentChannels(siteID: number): Promise { const resp = await api.get>('/admin/distribution/payment-channels', { params: { site_id: siteID }, }) return extract(resp).items ?? [] } export async function bindPaymentChannel(siteID: number, configID: number, priority: number): Promise { const resp = await api.post>( '/admin/distribution/payment-channels', { config_id: configID, priority }, { params: { site_id: siteID } }, ) return extract(resp) } export async function unbindPaymentChannel(id: number): Promise { await api.delete(`/admin/distribution/payment-channels/${id}`) } export async function listSettlementBatches(siteID: number): Promise { const resp = await api.get>('/admin/distribution/settlement-batches', { params: { site_id: siteID }, }) return extract(resp).items ?? [] } export async function createSettlementBatch(siteID: number, periodStart: string, periodEnd: string): Promise { const resp = await api.post>('/admin/distribution/settlement-batches', { site_id: siteID, period_start: periodStart, period_end: periodEnd, }) return extract(resp) } export async function settleBatch(id: number): Promise { const resp = await api.post>(`/admin/distribution/settlement-batches/${id}/settle`) return extract(resp) } export async function cancelSettlementBatch(id: number): Promise { const resp = await api.post>(`/admin/distribution/settlement-batches/${id}/cancel`) return extract(resp) } export async function listSettlementItems(batchID: number): Promise { const resp = await api.get>(`/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 seller_ratio?: number seller_total_price_cent?: number buyer_ratio?: 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 { const resp = await api.get>('/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 { 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 { const resp = await api.get>(`/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 { const resp = await api.get>(`/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 { const resp = await api.post>(`/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 aw_recycle_json?: string cooperation_feedback_json?: string storefront_text_json?: string listing_qrcode_url?: string } export async function uploadSiteImage(file: File, scene: string): Promise { const form = new FormData() form.append('file', file) form.append('scene', scene) const resp = await api.post>('/admin/files/upload', form, { headers: { 'Content-Type': 'multipart/form-data' }, }) return extract(resp).url } export async function updateSiteBranding(siteID: number, payload: SiteBrandingPayload): Promise { const resp = await api.put>(`/admin/distribution/sites/${siteID}/branding`, payload) return extract(resp) } export async function updateAccountMarkup(id: number, shelfRatioDelta: number): Promise { const resp = await api.put>(`/admin/distribution/accounts/${id}/markup`, { shelf_ratio_delta: shelfRatioDelta, }) return extract(resp) } export interface BatchUpdateAccountMarkupItem { id?: number listing_id?: number shelf_ratio_delta: number } export interface BatchUpdateAccountMarkupRequest { listing_ids?: number[] display_ids?: number[] shelf_ratio_delta?: number items?: BatchUpdateAccountMarkupItem[] } export async function batchUpdateAccountMarkup( siteID: number, reqOrListingIDs: BatchUpdateAccountMarkupRequest | number[], shelfRatioDelta?: number, ): Promise { const payload: BatchUpdateAccountMarkupRequest = Array.isArray(reqOrListingIDs) ? { listing_ids: reqOrListingIDs, shelf_ratio_delta: shelfRatioDelta ?? 0 } : reqOrListingIDs await api.post(`/admin/distribution/accounts/batch-markup`, payload, { params: { site_id: siteID }, }) } export async function batchUpdateAccountDisplay( siteID: number, displayIDs: number[], targetStatus: 'enabled' | 'disabled', ): Promise { await api.post( `/admin/distribution/accounts/batch-display`, { display_ids: displayIDs, target_status: targetStatus, }, { 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 { const resp = await api.get>(`/admin/distribution/sites/${siteID}/analytics`) return extract(resp) } export interface SiteSMSConfig { enabled: boolean provider: string access_key_id: string masked_secret: string has_secret: boolean endpoint: string sign_name: string login_template_code: string } export interface UpdateSiteSMSConfigPayload { enabled?: boolean provider?: string access_key_id?: string access_key_secret?: string endpoint?: string sign_name?: string login_template_code?: string } export interface TestSiteSMSConfigPayload { phone: string enabled?: boolean provider?: string access_key_id?: string access_key_secret?: string endpoint?: string sign_name?: string login_template_code?: string } export async function fetchSiteSMSConfig(siteID: number): Promise { const resp = await api.get>(`/admin/distribution/sites/${siteID}/sms-config`) return extract(resp) } export async function updateSiteSMSConfig(siteID: number, payload: UpdateSiteSMSConfigPayload): Promise { const resp = await api.put>(`/admin/distribution/sites/${siteID}/sms-config`, payload) return extract(resp) } export async function testSiteSMSConfig(siteID: number, payload: TestSiteSMSConfigPayload): Promise<{ message: string }> { const resp = await api.post>(`/admin/distribution/sites/${siteID}/sms-config/test`, payload) return extract(resp) }