- 交互重构:在「平台商户」与「平台账号」点击成员管理时,不再跳转路由或改写 localStorage.merchant_id,改为滑出 MerchantMembersDrawer 抽屉 - 抽屉能力:支持查看指定商户成员列表、新增成员(新建账号或关联已有账号)、修改角色与启用状态、移除成员 - 顶栏清理:彻底移除顶栏右上角的「当前商户:xxx」标签与「退出商户视角」按钮,超管登录自动清理任何残留上下文 - 后端补齐:新增平台专属 /api/platform/merchants/:id/members 与 roles 接口,平台操作不再依赖 X-Merchant-ID - 验证:tsc -b && vite build 成功;全量 go test ./... 包含路由与鉴权回归测试 100% 通过
355 lines
17 KiB
TypeScript
355 lines
17 KiB
TypeScript
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),
|
||
}
|
||
|
||
|
||
|