import axios from 'axios' import { optimizeImageForUpload } from './utils/imageUpload' 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 announcement_mode?: 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 user_count?: number order_user_count?: number 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 /** 公告中心来源:inherit=继承主站+本站,own_only=仅本站 */ announcement_mode?: string listing_qrcode_url?: string } // ---------------- 分站公告中心(站长自助维护本站公告) ---------------- export interface SiteAnnouncement { id: number distribution_site_id: number title: string content: string category: string priority: number is_pinned: boolean is_important: boolean /** 弹窗公告:本站首页弹出且需用户手动关闭,同一天只弹一次 */ is_popup: boolean view_count: number status: string published_at: string | null created_at: string updated_at: string } export interface SiteAnnouncementPage { items: SiteAnnouncement[] total: number page: number page_size: number } export interface SiteAnnouncementPayload { title: string content: string category: string priority: number is_pinned: boolean is_important: boolean is_popup: boolean } export async function listSiteAnnouncements( siteID: number, params: { status?: string; page?: number; page_size?: number } = {}, ): Promise { const resp = await api.get>('/admin/distribution/announcements', { params: { site_id: siteID, ...params }, }) return extract(resp) } export async function createSiteAnnouncement( siteID: number, payload: SiteAnnouncementPayload, ): Promise { const resp = await api.post>( '/admin/distribution/announcements', payload, { params: { site_id: siteID } }, ) return extract(resp) } export async function updateSiteAnnouncement( siteID: number, id: number, payload: SiteAnnouncementPayload, ): Promise { const resp = await api.put>( `/admin/distribution/announcements/${id}`, payload, { params: { site_id: siteID } }, ) return extract(resp) } export async function updateSiteAnnouncementStatus( siteID: number, id: number, action: 'publish' | 'archive' | 'unarchive', ): Promise { const resp = await api.post>( `/admin/distribution/announcements/${id}/${action}`, {}, { params: { site_id: siteID } }, ) return extract(resp) } export async function deleteSiteAnnouncement(siteID: number, id: number): Promise { await api.delete(`/admin/distribution/announcements/${id}`, { params: { site_id: siteID } }) } export async function uploadSiteImage(file: File, scene: string): Promise { const uploadTarget = await optimizeImageForUpload(file, scene) const form = new FormData() form.append('file', uploadTarget) 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 today_user_count?: number total_user_count?: number total_order_user_count?: 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 DistributionUser { id: number phone: string nickname: string avatar_url: string realname_status: string status: string source: 'register' | 'order' | 'both' order_count: number total_spent_cent: number last_order_at?: string created_at: string last_login_at?: string } export interface DistributionUserPage { items: DistributionUser[] total: number page: number page_size: number total_users: number order_users: number } export interface ListDistributionUsersParams { siteID?: number keyword?: string source?: 'all' | 'register' | 'order' page?: number pageSize?: number } export async function listDistributionUsers( params: ListDistributionUsersParams = {} ): Promise { const resp = await api.get>('/admin/distribution/users', { params: { site_id: params.siteID, keyword: params.keyword, source: params.source, page: params.page, page_size: params.pageSize, }, }) 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) } export interface InvitationRebateConfig { distribution_site_id: number first_month_rate: number second_month_rate: number subsequent_rate: number enabled: boolean updated_at?: string } export interface InvitedUserItem { id: number inviter_id: number inviter_phone?: string inviter_nickname?: string invitee_id: number invitee_phone?: string invitee_nickname?: string invite_code: string status: string verified_at?: string created_at: string } export interface RebateLedgerItem { id: number order_id: number order_no: string inviter_id: number inviter_phone?: string inviter_nickname?: string invitee_id: number invitee_phone?: string invitee_nickname?: string trade_role: 'buyer' | 'seller' order_profit_basis_cent: number single_side_basis_cent: number rate_applied: number days_since_verified: number cycle_stage: string rebate_amount_cent: number status: string settled_at: string } export async function fetchSiteInvitationConfig(siteID?: number): Promise { const url = siteID ? `/distribution/sites/${siteID}/invitation-config` : '/distribution/invitation-config' const resp = await api.get>(url) return extract(resp) } export async function updateSiteInvitationConfig( siteID: number, payload: { first_month_rate: number second_month_rate: number subsequent_rate: number enabled: boolean } ): Promise { const url = siteID ? `/distribution/sites/${siteID}/invitation-config` : '/distribution/invitation-config' const resp = await api.put>(url, payload) return extract(resp) } export async function fetchSiteInvitations( siteID: number, params: { page: number; page_size: number; phone?: string; invite_code?: string } ): Promise<{ items: InvitedUserItem[]; total: number; page: number; page_size: number }> { const url = siteID ? `/distribution/sites/${siteID}/invitations` : '/distribution/invitations' const resp = await api.get>(url, { params }) return extract(resp) } export async function fetchSiteRebates( siteID: number, params: { page: number; page_size: number; order_no?: string; inviter_phone?: string; invitee_phone?: string } ): Promise<{ items: RebateLedgerItem[]; total: number; page: number; page_size: number }> { const url = siteID ? `/distribution/sites/${siteID}/invitation-rebates` : '/distribution/invitation-rebates' const resp = await api.get>(url, { params }) return extract(resp) }