Files
hfb_sys/distribution-admin/src/api.ts
T
yml2213andCursor 768902f2f7 feat(distribution): 补齐退款加价回退、本站自卖与开通默认规则
退款时压缩卖方加价,人工/自动批次仅完结单可入账,号主收入进主站钱包;仲裁后重算释放并支持作废未入账 DS。开通时四种模式按默认生效,对外口径合并为 35/40/25,不再单独展示通道费。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-10 18:13:46 +08:00

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)
}