Files
affiliate_dash/frontend/src/api/index.ts
T
yml2213 8cc5d42d53 feat(admin): 平台商户成员管理改用抽屉,消除商户视角切换与全局状态污染
- 交互重构:在「平台商户」与「平台账号」点击成员管理时,不再跳转路由或改写 localStorage.merchant_id,改为滑出 MerchantMembersDrawer 抽屉
- 抽屉能力:支持查看指定商户成员列表、新增成员(新建账号或关联已有账号)、修改角色与启用状态、移除成员
- 顶栏清理:彻底移除顶栏右上角的「当前商户:xxx」标签与「退出商户视角」按钮,超管登录自动清理任何残留上下文
- 后端补齐:新增平台专属 /api/platform/merchants/:id/members 与 roles 接口,平台操作不再依赖 X-Merchant-ID
- 验证:tsc -b && vite build 成功;全量 go test ./... 包含路由与鉴权回归测试 100% 通过
2026-10-06 21:13:37 +08:00

355 lines
17 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import request from './request'
import type {
DashboardStats,
AlertChannelInput,
AlertNotifyChannel,
ApiClient,
ApiCredential,
CallbackCredential,
CallbackSubscription,
CreateManualOrderResult,
CreateTestOrderResult,
DeliveryBindResult,
DeliveryOrderInfo,
DeliveryLinkResult,
DeliverySubmitResult,
FulfillmentOrder,
LoginResult,
LowBalanceAlertConfig,
Merchant,
MerchantMember,
MerchantMemberGroup,
MerchantOption,
MerchantOwnerAccount,
MerchantRole,
MerchantProduct,
PageResult,
ProductCatalogItem,
RechargeApplication,
User,
WalletAccount,
WalletLedgerEntry,
LewanProductListResult,
UpdateProductRoutingInput,
SyncLewanProductInput,
} from '../types'
export const authApi = {
login: async (username: string, password: string) => {
const res = await request.post('/auth/login', { username, password })
return res.data.data as LoginResult
},
profile: async () => {
const res = await request.get('/auth/profile')
return res.data.data as User
},
changePassword: (data: { current_password: string; new_password: string }) =>
request.put('/auth/password', data).then((r) => r.data.data),
updateCurrentAccount: (data: { current_password: string; username: string; nickname?: string; new_password?: string }) =>
request.put('/auth/account', data).then((r) => r.data.data as User),
}
export const dashboardApi = {
stats: () =>
request.get('/dashboard').then((r) => r.data.data as DashboardStats),
}
export const userApi = {
list: (params?: Record<string, unknown>) =>
request.get('/platform/admins', { params }).then((r) => r.data.data as PageResult<User>),
create: (data: { username: string; password: string; nickname?: string }) =>
request.post('/platform/admins', data).then((r) => r.data.data as User),
update: (id: number, data: { username: string; nickname?: string; password?: string; status: number }) =>
request.patch(`/platform/admins/${id}`, data).then((r) => r.data.data as User),
remove: (id: number) =>
request.delete(`/platform/admins/${id}`).then((r) => r.data.data),
}
export const merchantApi = {
current: () =>
request.get('/merchant').then((r) => r.data.data as { merchant: Merchant; role: string; permissions: string[] }),
products: (params?: Record<string, unknown>) =>
request.get('/merchant/products', { params }).then((r) => r.data.data as PageResult<MerchantProduct>),
updateProduct: (
id: number,
data: {
display_name?: string
price_amount?: number
cost_amount?: number
stock?: number
status?: 'active' | 'inactive'
}
) => request.patch(`/merchant/products/${id}`, data).then((r) => r.data.data),
updateProductStatus: (id: number, status: MerchantProduct['status']) =>
request.patch(`/merchant/products/${id}/status`, { status }).then((r) => r.data.data),
deleteProduct: (id: number) =>
request.delete(`/merchant/products/${id}`).then((r) => r.data.data),
orders: (params?: Record<string, unknown>) =>
request.get('/merchant/orders', { params }).then((r) => r.data.data as PageResult<FulfillmentOrder>),
manualOrderProducts: () =>
request.get('/merchant/orders/products').then((r) => r.data.data as MerchantProduct[]),
createTestOrder: (data: {
sku: string
buyer_reference?: string
note?: string
order_status?: 'paid' | 'ship_failed' | 'cancelled'
}) => request.post('/merchant/orders/test', data).then((r) => r.data.data as CreateTestOrderResult),
createManualOrder: (data: {
client_order_no: string
sku: string
quantity: number
buyer_reference?: string
game_account?: string
note?: string
}) => request.post('/merchant/orders/manual', data).then((r) => r.data.data as CreateManualOrderResult),
cancelOrder: (orderNo: string, reason: string) =>
request.post(`/merchant/orders/${encodeURIComponent(orderNo)}/cancel`, { reason }).then((r) => r.data.data as { order: FulfillmentOrder; refund_amount: number }),
getDeliveryLink: (orderNo: string) =>
request.get(`/merchant/orders/${encodeURIComponent(orderNo)}/delivery-link`).then((r) => r.data.data as DeliveryLinkResult),
revokeDeliveryLink: (orderNo: string) =>
request.post(`/merchant/orders/${encodeURIComponent(orderNo)}/delivery-link/revoke`).then((r) => r.data.data),
restoreDeliveryLink: (orderNo: string) =>
request.post(`/merchant/orders/${encodeURIComponent(orderNo)}/delivery-link/restore`).then((r) => r.data.data as DeliveryLinkResult),
wallet: () =>
request.get('/merchant/wallet').then((r) => r.data.data as WalletAccount),
ledger: (params?: Record<string, unknown>) =>
request.get('/merchant/wallet/ledger', { params }).then((r) => r.data.data as PageResult<WalletLedgerEntry>),
apiClients: () =>
request.get('/merchant/api-clients').then((r) => r.data.data as ApiClient[]),
createApiClient: (data: {
name: string
scopes: string
signature_version?: string
expires_at?: string
}) => request.post('/merchant/api-clients', data).then((r) => r.data.data as ApiCredential),
updateApiClientStatus: (id: number, status: ApiClient['status']) =>
request.patch(`/merchant/api-clients/${id}/status`, { status }).then((r) => r.data.data),
deleteApiClient: (id: number) =>
request.delete(`/merchant/api-clients/${id}`).then((r) => r.data.data),
callbacks: () =>
request.get('/merchant/callbacks').then((r) => r.data.data as CallbackSubscription | null),
saveCallback: (data: { url: string; events: string; status: CallbackSubscription['status']; rotate_secret?: boolean }) =>
request.post('/merchant/callbacks', data).then((r) => r.data.data as CallbackCredential),
members: () =>
request.get('/merchant/members').then((r) => r.data.data as MerchantMember[]),
addMember: (data: { user_id?: number; username?: string; password?: string; nickname?: string; role: string; is_default?: boolean }) =>
request.post('/merchant/members', data).then((r) => r.data.data as MerchantMember),
updateMember: (id: number, data: { role: string; status: number; is_default: boolean }) =>
request.patch(`/merchant/members/${id}`, data).then((r) => r.data.data as MerchantMember),
removeMember: (id: number) =>
request.delete(`/merchant/members/${id}`).then((r) => r.data.data),
roles: () => request.get('/merchant/roles').then((r) => r.data.data as MerchantRole[]),
createRole: (data: { code: string; name: string; permissions: string[] }) =>
request.post('/merchant/roles', data).then((r) => r.data.data as MerchantRole),
updateRole: (id: number, data: { name: string; permissions: string[] }) =>
request.patch(`/merchant/roles/${id}`, data).then((r) => r.data.data as MerchantRole),
rechargeApplications: (params?: Record<string, unknown>) =>
request.get('/merchant/recharge/applications', { params }).then((r) => r.data.data as PageResult<RechargeApplication>),
createRechargeApplication: (data: { amount_cny: number; vouchers: string[]; note?: string }) =>
// amount_cny 单位:分(1元 = 100分),积分按 1元 = 100积分 折算
request.post('/merchant/recharge/applications', data).then((r) => r.data.data as RechargeApplication),
alertConfig: () =>
request.get('/merchant/recharge/alert-config').then((r) => r.data.data as LowBalanceAlertConfig),
saveAlertConfig: (data: { enabled: boolean; threshold_points: number; notify_interval_minutes: number; max_notifications: number }) =>
request.put('/merchant/recharge/alert-config', data).then((r) => r.data.data as LowBalanceAlertConfig),
alertChannels: () =>
request.get('/merchant/alert-channels').then((r) => r.data.data as AlertNotifyChannel[]),
createAlertChannel: (data: AlertChannelInput) =>
request.post('/merchant/alert-channels', data).then((r) => r.data.data as AlertNotifyChannel),
updateAlertChannel: (id: number, data: AlertChannelInput) =>
request.put(`/merchant/alert-channels/${id}`, data).then((r) => r.data.data as AlertNotifyChannel),
deleteAlertChannel: (id: number) =>
request.delete(`/merchant/alert-channels/${id}`).then((r) => r.data.data),
testAlertChannel: (id: number) =>
request.post(`/merchant/alert-channels/${id}/test`).then((r) => r.data.data),
}
export const uploadApi = {
file: (file: File) => {
const form = new FormData()
form.append('file', file)
return request.post('/upload', form, {
headers: { 'Content-Type': 'multipart/form-data' },
timeout: 60000,
}).then((r) => r.data.data as { url: string })
},
}
export const deliveryApi = {
getOrder: (orderNo: string, auth?: { exp?: string; sign?: string }) =>
request.get(`/delivery/v1/orders/${encodeURIComponent(orderNo)}`, { params: auth }).then((r) => r.data.data as DeliveryOrderInfo),
bind: (orderNo: string, gameAccount: string, auth?: { exp?: string; sign?: string }) =>
request.post(
`/delivery/v1/orders/${encodeURIComponent(orderNo)}/bind`,
{
game_account: gameAccount,
},
{ params: auth },
).then((r) => r.data.data as DeliveryBindResult),
getBindResult: (orderNo: string, bindUUID: string, auth?: { exp?: string; sign?: string }) =>
request.get(`/delivery/v1/orders/${encodeURIComponent(orderNo)}/bind-result`, {
params: { ...auth, bind_uuid: bindUUID },
}).then((r) => r.data.data as DeliveryBindResult),
submit: (orderNo: string, gameAccount: string, bindUUID: string, auth?: { exp?: string; sign?: string }) =>
request.post(
`/delivery/v1/orders/${encodeURIComponent(orderNo)}/submit`,
{
game_account: gameAccount,
bind_uuid: bindUUID,
},
{ params: auth },
).then((r) => r.data.data as DeliverySubmitResult),
}
export const platformApi = {
merchantAccounts: (params?: Record<string, unknown>) =>
request.get('/platform/merchant-accounts', { params }).then((r) => r.data.data as PageResult<MerchantMemberGroup>),
deleteMerchantAccount: (userId: number) =>
request.delete(`/platform/merchant-accounts/${userId}`).then((r) => r.data.data),
merchants: (params?: Record<string, unknown>) =>
request.get('/platform/merchants', { params }).then((r) => r.data.data as PageResult<Merchant>),
// 不分页的精简商户列表,供筛选下拉使用(/platform/merchants 的 size 上限是 100)
merchantOptions: () =>
request.get('/platform/merchant-options').then((r) => r.data.data as MerchantOption[]),
// 平台侧跨商户订单视图:不传 merchant_id 即全部商户
orders: (params?: Record<string, unknown>) =>
request.get('/platform/orders', { params }).then((r) => r.data.data as PageResult<FulfillmentOrder>),
createMerchant: (data: {
code: string
name: string
contact_name?: string
contact_info?: string
owner_user_id?: number
owner_username?: string
owner_password?: string
owner_nickname?: string
features?: string
fee_type?: 'rate' | 'fixed'
fee_rate_bp?: number
fee_fixed_amount?: number
}) => request.post('/platform/merchants', data).then((r) => r.data.data as Merchant),
updateMerchant: (id: number, data: Partial<Merchant>) =>
request.patch(`/platform/merchants/${id}`, data).then((r) => r.data.data),
members: (merchantId: number) =>
request.get(`/platform/merchants/${merchantId}/members`).then((r) => r.data.data as MerchantMember[]),
addMember: (
merchantId: number,
data: { user_id?: number; username?: string; password?: string; nickname?: string; role: string; is_default?: boolean },
) => request.post(`/platform/merchants/${merchantId}/members`, data).then((r) => r.data.data as MerchantMember),
updateMember: (
merchantId: number,
memberId: number,
data: { role: string; status: number; is_default: boolean },
) => request.patch(`/platform/merchants/${merchantId}/members/${memberId}`, data).then((r) => r.data.data as MerchantMember),
removeMember: (merchantId: number, memberId: number) =>
request.delete(`/platform/merchants/${merchantId}/members/${memberId}`).then((r) => r.data.data),
roles: (merchantId: number) =>
request.get(`/platform/merchants/${merchantId}/roles`).then((r) => r.data.data as MerchantRole[]),
productCatalog: () =>
request.get('/platform/product-catalog').then((r) => r.data.data as ProductCatalogItem[]),
merchantProducts: (merchantId: number) =>
request.get(`/platform/merchants/${merchantId}/products`).then((r) => r.data.data as ProductCatalogItem[]),
updateMerchantProduct: (merchantId: number, productId: number, data: Partial<MerchantProduct>) =>
request.patch(`/platform/merchants/${merchantId}/products/${productId}`, data).then((r) => r.data.data),
assignProducts: (merchantId: number, catalogIds: number[]) =>
request.post(`/platform/merchants/${merchantId}/products/assign`, { catalog_ids: catalogIds }).then((r) => r.data.data as { assigned: number }),
wallet: (merchantId: number) =>
request.get(`/platform/merchants/${merchantId}/wallet`).then((r) => r.data.data as WalletAccount),
merchantAccount: (merchantId: number) =>
request.get(`/platform/merchants/${merchantId}/account`).then((r) => r.data.data as MerchantOwnerAccount[]),
updateMerchantAccount: (merchantId: number, data: { user_id: number; username: string; nickname?: string; password?: string; status?: number }) =>
request.patch(`/platform/merchants/${merchantId}/account`, data).then((r) => r.data.data as MerchantOwnerAccount),
walletLedger: (merchantId: number, params?: Record<string, unknown>) =>
request.get(`/platform/merchants/${merchantId}/wallet/ledger`, { params }).then((r) => r.data.data as PageResult<WalletLedgerEntry>),
adjustWallet: (merchantId: number, data: { amount: number; idempotency_key: string; note?: string }) =>
request.post(`/platform/merchants/${merchantId}/wallet/adjust`, data).then((r) => r.data.data as WalletAccount),
rechargeApplications: (params?: Record<string, unknown>) =>
request.get('/platform/recharge/applications', { params }).then((r) => r.data.data as PageResult<RechargeApplication>),
reviewRechargeApplication: (id: number, data: { approved: boolean; review_note?: string }) =>
request.post(`/platform/recharge/applications/${id}/review`, data).then((r) => r.data.data as RechargeApplication),
}
export const lewanApi = {
listProducts: (params?: { page?: number; size?: number; keyword?: string }) =>
request.get('/platform/lewan/products', { params }).then((r) => r.data.data as LewanProductListResult),
syncCatalog: (data: SyncLewanProductInput) =>
request.post('/platform/lewan/sync-catalog', data).then((r) => r.data.data as ProductCatalogItem),
batchSyncCatalog: (products: SyncLewanProductInput[]) =>
request.post('/platform/lewan/batch-sync-catalog', { products }).then((r) => r.data.data as { synced_count: number }),
updateRouting: (id: number, data: UpdateProductRoutingInput) =>
request.put(`/platform/products/${id}/routing`, data).then((r) => r.data.data as { updated: boolean }),
}
export interface RockConfig {
id: number
auto_delivery_enabled: boolean
base_url: string
bearer_token: string
username: string
points: number
min_points_warning: number
token_warning_stock: number
auto_purchase_enabled: boolean
auto_purchase_count: number
purchase_cooldown_minutes: number
last_purchased_at?: string
last_heartbeat_at?: string
}
export interface RockStats {
available_stock: number
using_stock: number
bound_stock: number
redeemed_stock: number
mismatch_stock: number
abnormal_stock: number
upstream_points: number
auto_delivery_enabled: boolean
token_warning_stock: number
auto_purchase_enabled: boolean
username: string
last_heartbeat_at?: string
}
export interface RockTokenItem {
id: number
upstream_token_id: string
redeem_code: string
color: string
region: string
status: string
assigned_order_no?: string
client_order_no?: string
expected_uid?: string
actual_role_name?: string
actual_role_id?: string
actual_channel?: string
upstream_fulfill_id?: number
target_skin?: string
target_currency?: string
target_currency_quantity?: number
mismatch_at?: string
mismatch_reason?: string
claimed_at?: string
bound_at?: string
redeemed_at?: string
created_at: string
}
export const rockApi = {
getConfig: () => request.get('/platform/rock/config').then((r) => r.data.data as RockConfig),
updateConfig: (data: Partial<RockConfig>) => request.put('/platform/rock/config', data).then((r) => r.data.data as RockConfig),
testConnection: (bearer_token?: string) => request.post('/platform/rock/test-conn', { bearer_token }).then((r) => r.data.data),
getStats: () => request.get('/platform/rock/stats').then((r) => r.data.data as RockStats),
purchaseTokens: (count = 50) => request.post('/platform/rock/purchase', { count }).then((r) => r.data.data),
listTokens: (params?: { page?: number; page_size?: number; status?: string; search?: string }) =>
request.get('/platform/rock/tokens', { params }).then((r) => r.data.data as { list: RockTokenItem[]; total: number; page: number; page_size: number }),
correctExpectedUid: (id: number, expectedUid: string) =>
request.post(`/platform/rock/tokens/${id}/expected-uid`, { expected_uid: expectedUid }).then((r) => r.data.data as { message?: string; status?: string }),
resetToken: (id: number) => request.post(`/platform/rock/tokens/${id}/reset`).then((r) => r.data.data),
releaseToken: (id: number) => request.post(`/platform/rock/tokens/${id}/release`).then((r) => r.data.data),
}