chore: initial commit
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
import { apiRequest } from '@/api/http'
|
||||
import type { TokenPair } from '@/api/session'
|
||||
|
||||
export interface RegisterMerchantRequest {
|
||||
username: string
|
||||
password: string
|
||||
displayName: string
|
||||
companyName: string
|
||||
legalPerson: string
|
||||
legalPhone: string
|
||||
contactEmail?: string
|
||||
countryCode?: string
|
||||
address?: string
|
||||
}
|
||||
|
||||
export interface RegisterMerchantResult {
|
||||
accountId: number | string
|
||||
tenantId: number | string
|
||||
membershipId: number | string
|
||||
tenantStatus: 'PROFILE_REQUIRED'
|
||||
nextStep: string
|
||||
}
|
||||
|
||||
export function loginMerchant(username: string, password: string) {
|
||||
return apiRequest<TokenPair>({ method: 'POST', url: '/auth/login', data: { username, password } })
|
||||
}
|
||||
|
||||
export function registerMerchant(payload: RegisterMerchantRequest) {
|
||||
return apiRequest<RegisterMerchantResult>({ method: 'POST', url: '/auth/register', data: payload })
|
||||
}
|
||||
|
||||
export function logoutMerchant(refreshToken: string) {
|
||||
return apiRequest<void>({ method: 'POST', url: '/auth/logout', data: { refreshToken } })
|
||||
}
|
||||
|
||||
export function changeCurrentPassword(oldPassword: string, newPassword: string) {
|
||||
return apiRequest<void>({ method: 'PUT', url: '/auth/password', data: { oldPassword, newPassword } })
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
import { apiRequest } from '@/api/http'
|
||||
|
||||
export type ApiFirstLegStatus = 'DRAFT' | 'SUBMITTED' | 'APPROVED' | 'ACCEPTED' | 'REJECTED' | 'CANCELLED' | 'PROCESSING' | 'COMPLETED'
|
||||
export type ApiTransportMode = 'SEA' | 'AIR' | 'RAIL' | 'TRUCK'
|
||||
export type ApiPickupMethod = 'SELF_EXPRESS' | 'PICKUP' | 'SELF_TRUCK'
|
||||
export type ApiTaxMode = 'BUY_DECLARATION' | 'TAX_REFUND'
|
||||
export type ApiContainerMode = 'FULL_CONTAINER' | 'LCL'
|
||||
export type ApiDestinationType = 'FBA' | 'OVERSEAS' | 'PRIVATE'
|
||||
|
||||
export interface PageResult<T> {
|
||||
records: T[]
|
||||
page: number
|
||||
size: number
|
||||
total: number
|
||||
pages: number
|
||||
}
|
||||
|
||||
export interface MerchantWarehouse {
|
||||
id: number | string
|
||||
warehouseCode: string
|
||||
warehouseName: string
|
||||
warehouseType: string
|
||||
countryCode: string
|
||||
timezone: string | null
|
||||
contactName: string | null
|
||||
contactPhone: string | null
|
||||
contactEmail: string | null
|
||||
status: 'ACTIVE' | 'INACTIVE'
|
||||
remark: string | null
|
||||
updatedAt: string | null
|
||||
}
|
||||
|
||||
export interface MerchantWarehouseAddress {
|
||||
countryCode: string
|
||||
stateProvince: string | null
|
||||
city: string | null
|
||||
district: string | null
|
||||
postalCode: string | null
|
||||
addressLine1: string
|
||||
addressLine2: string | null
|
||||
}
|
||||
|
||||
export interface MerchantWarehouseCapability {
|
||||
capabilityCode: string
|
||||
capabilityName: string
|
||||
enabled: boolean
|
||||
cutoffTime: string | null
|
||||
description: string | null
|
||||
}
|
||||
|
||||
export interface MerchantWarehouseDetail {
|
||||
warehouse: MerchantWarehouse
|
||||
addresses: MerchantWarehouseAddress[]
|
||||
capabilities: MerchantWarehouseCapability[]
|
||||
}
|
||||
|
||||
export interface MerchantPartner {
|
||||
id: number | string
|
||||
partnerCode: string
|
||||
partnerName: string
|
||||
partnerType: string
|
||||
contactName: string | null
|
||||
contactPhone: string | null
|
||||
contactEmail: string | null
|
||||
countryCode: string
|
||||
province: string | null
|
||||
city: string | null
|
||||
district: string | null
|
||||
addressLine: string
|
||||
postalCode: string | null
|
||||
status: 'ACTIVE' | 'INACTIVE'
|
||||
}
|
||||
|
||||
export interface FirstLegOrderPartyPayload {
|
||||
companyName: string
|
||||
contactName: string
|
||||
phone: string
|
||||
email?: string
|
||||
countryCode: string
|
||||
province?: string
|
||||
city: string
|
||||
district?: string
|
||||
addressLine: string
|
||||
postalCode?: string
|
||||
}
|
||||
|
||||
export interface FirstLegOrderCargoPayload {
|
||||
cargoName: string
|
||||
hsCode?: string
|
||||
packageType: string
|
||||
packageCount: number
|
||||
grossWeightKg: number
|
||||
volumeCbm: number
|
||||
declaredValue?: number
|
||||
currency?: string
|
||||
dangerousGoods?: boolean
|
||||
cargoRemark?: string
|
||||
skuNo?: string
|
||||
cargoNameEn?: string
|
||||
totalQuantity?: number
|
||||
netWeightKg?: number
|
||||
}
|
||||
|
||||
export interface FirstLegOrderSaveRequest {
|
||||
customerId?: number | string
|
||||
customerName: string
|
||||
transportMode: ApiTransportMode
|
||||
tradeTerm?: string
|
||||
destinationCountryCode: string
|
||||
destinationWarehouseId?: number | string
|
||||
destinationWarehouseName: string
|
||||
expectedShipDate?: string
|
||||
customerRemark?: string
|
||||
sender: FirstLegOrderPartyPayload
|
||||
receiver: FirstLegOrderPartyPayload
|
||||
cargoes: FirstLegOrderCargoPayload[]
|
||||
pickupMethod: ApiPickupMethod
|
||||
receivingWarehouseId: number | string
|
||||
receivingWarehouseName: string
|
||||
receivingWarehouseAddress?: string | null
|
||||
receivingTime?: string | null
|
||||
channelCode: string
|
||||
channelName: string
|
||||
taxMode: ApiTaxMode
|
||||
containerMode?: ApiContainerMode
|
||||
customsInspection: boolean
|
||||
destinationType: ApiDestinationType
|
||||
amazonWarehouseCode?: string
|
||||
shipmentId?: string
|
||||
referenceId?: string
|
||||
}
|
||||
|
||||
export interface MerchantFirstLegOrder {
|
||||
id: number | string
|
||||
orderNo: string | null
|
||||
customerId: number | string | null
|
||||
customerName: string
|
||||
status: ApiFirstLegStatus
|
||||
transportMode: ApiTransportMode
|
||||
tradeTerm: string | null
|
||||
destinationCountryCode: string
|
||||
destinationWarehouseId: number | string | null
|
||||
destinationWarehouseName: string
|
||||
expectedShipDate: string | null
|
||||
customerRemark: string | null
|
||||
pickupMethod: ApiPickupMethod
|
||||
receivingWarehouseId: number | string
|
||||
receivingWarehouseName: string
|
||||
channelCode: string
|
||||
channelName: string
|
||||
taxMode: ApiTaxMode
|
||||
containerMode: ApiContainerMode | null
|
||||
customsInspection: boolean
|
||||
destinationType: ApiDestinationType
|
||||
amazonWarehouseCode: string | null
|
||||
shipmentId: string | null
|
||||
referenceId: string | null
|
||||
expressCompany?: string | null
|
||||
expressTrackingNo?: string | null
|
||||
version: number
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
export interface MerchantFirstLegOrderCargo extends FirstLegOrderCargoPayload {
|
||||
id: number | string
|
||||
itemNo: number
|
||||
}
|
||||
|
||||
export interface MerchantFirstLegOrderParty extends FirstLegOrderPartyPayload {
|
||||
id: number | string
|
||||
partyType: 'SENDER' | 'RECEIVER' | string
|
||||
}
|
||||
|
||||
export interface MerchantOrderStatusHistory {
|
||||
id: number | string
|
||||
fromStatus: ApiFirstLegStatus | null
|
||||
toStatus: ApiFirstLegStatus
|
||||
action: string
|
||||
reason: string | null
|
||||
operatorName: string | null
|
||||
occurredAt: string
|
||||
}
|
||||
|
||||
export interface MerchantFirstLegOrderDetail {
|
||||
order: MerchantFirstLegOrder
|
||||
cargoes: MerchantFirstLegOrderCargo[]
|
||||
parties: MerchantFirstLegOrderParty[]
|
||||
internalNotes: []
|
||||
statusHistory: MerchantOrderStatusHistory[]
|
||||
}
|
||||
|
||||
export interface FirstLegOrderQuery {
|
||||
keyword?: string
|
||||
status?: ApiFirstLegStatus
|
||||
customerId?: number | string
|
||||
destinationWarehouseId?: number | string
|
||||
createdFrom?: string
|
||||
createdTo?: string
|
||||
page?: number
|
||||
size?: number
|
||||
}
|
||||
|
||||
export function getMerchantWarehouses() {
|
||||
return apiRequest<PageResult<MerchantWarehouse>>({ method: 'GET', url: '/merchant/v1/warehouses', params: { page: 1, size: 200, status: 'ACTIVE' } })
|
||||
}
|
||||
|
||||
export function getMerchantWarehouseDetail(id: number | string) {
|
||||
return apiRequest<MerchantWarehouseDetail>({ method: 'GET', url: `/merchant/v1/warehouses/${id}` })
|
||||
}
|
||||
|
||||
export function getMerchantPartnerCandidates(type: 'SHIPPER' | 'CONSIGNEE') {
|
||||
return apiRequest<MerchantPartner[]>({ method: 'GET', url: '/merchant/v1/partners/candidates', params: { type } })
|
||||
}
|
||||
|
||||
export function getMerchantFirstLegOrders(params: FirstLegOrderQuery = {}) {
|
||||
return apiRequest<PageResult<MerchantFirstLegOrder>>({ method: 'GET', url: '/merchant/v1/orders/first-leg', params: { page: 1, size: 200, ...params } })
|
||||
}
|
||||
|
||||
export function getMerchantFirstLegOrder(id: number | string) {
|
||||
return apiRequest<MerchantFirstLegOrderDetail>({ method: 'GET', url: `/merchant/v1/orders/first-leg/${id}` })
|
||||
}
|
||||
|
||||
export function createMerchantFirstLegOrder(payload: FirstLegOrderSaveRequest) {
|
||||
return apiRequest<number | string>({
|
||||
method: 'POST',
|
||||
url: '/merchant/v1/orders/first-leg',
|
||||
data: payload,
|
||||
headers: { 'Idempotency-Key': createIdempotencyKey('first-leg-create') },
|
||||
})
|
||||
}
|
||||
|
||||
export function updateMerchantFirstLegOrder(id: number | string, payload: FirstLegOrderSaveRequest) {
|
||||
return apiRequest<void>({ method: 'PUT', url: `/merchant/v1/orders/first-leg/${id}`, data: payload })
|
||||
}
|
||||
|
||||
export function submitMerchantFirstLegOrder(id: number | string) {
|
||||
return apiRequest<void>({
|
||||
method: 'POST',
|
||||
url: `/merchant/v1/orders/first-leg/${id}/submit`,
|
||||
headers: { 'Idempotency-Key': createIdempotencyKey(`first-leg-submit-${id}`) },
|
||||
})
|
||||
}
|
||||
|
||||
function createIdempotencyKey(scope: string) {
|
||||
const randomPart = globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(36).slice(2)}`
|
||||
return `${scope}-${randomPart}`
|
||||
}
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
import axios, { AxiosError, type AxiosRequestConfig, type InternalAxiosRequestConfig } from 'axios'
|
||||
import { clearSessionTokens, getAccessToken, getRefreshToken, persistTokenPair, type TokenPair } from '@/api/session'
|
||||
import { ApiError, asFieldErrors, isApiEnvelope, type ApiEnvelope } from '@/api/types'
|
||||
|
||||
const transport = axios.create({
|
||||
baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
|
||||
timeout: 15_000,
|
||||
})
|
||||
|
||||
interface RetryableRequest extends InternalAxiosRequestConfig {
|
||||
_retriedAfterRefresh?: boolean
|
||||
}
|
||||
|
||||
function unwrap<T>(envelope: ApiEnvelope<T>) {
|
||||
if (envelope.code !== '0') {
|
||||
throw new ApiError(envelope.message || '请求失败', envelope.code, undefined, asFieldErrors(envelope.data), envelope.traceId)
|
||||
}
|
||||
return envelope.data
|
||||
}
|
||||
|
||||
function mapError(error: unknown) {
|
||||
if (error instanceof ApiError) return error
|
||||
if (!axios.isAxiosError(error)) return new ApiError(error instanceof Error ? error.message : '请求失败')
|
||||
|
||||
const body = error.response?.data
|
||||
if (isApiEnvelope(body)) {
|
||||
return new ApiError(
|
||||
body.message || '请求失败',
|
||||
body.code,
|
||||
error.response?.status,
|
||||
asFieldErrors(body.data),
|
||||
typeof body.traceId === 'string' ? body.traceId : null,
|
||||
)
|
||||
}
|
||||
if (error.code === AxiosError.ERR_NETWORK) return new ApiError('无法连接服务器,请确认后端服务已启动', 'NETWORK_ERROR')
|
||||
if (error.code === AxiosError.ECONNABORTED) return new ApiError('请求超时,请稍后重试', 'TIMEOUT')
|
||||
return new ApiError(error.message || '请求失败', 'HTTP_ERROR', error.response?.status)
|
||||
}
|
||||
|
||||
let refreshPromise: Promise<TokenPair> | null = null
|
||||
|
||||
async function refreshSession() {
|
||||
const refreshToken = getRefreshToken()
|
||||
if (!refreshToken) throw new ApiError('登录已过期,请重新登录', 'A0401', 401)
|
||||
if (!refreshPromise) {
|
||||
refreshPromise = transport
|
||||
.post<ApiEnvelope<TokenPair>>('/auth/refresh', { refreshToken })
|
||||
.then((response) => unwrap(response.data))
|
||||
.then((tokens) => {
|
||||
persistTokenPair(tokens)
|
||||
return tokens
|
||||
})
|
||||
.finally(() => {
|
||||
refreshPromise = null
|
||||
})
|
||||
}
|
||||
return refreshPromise
|
||||
}
|
||||
|
||||
transport.interceptors.request.use((config) => {
|
||||
const token = getAccessToken()
|
||||
if (token) config.headers.Authorization = `Bearer ${token}`
|
||||
return config
|
||||
})
|
||||
|
||||
transport.interceptors.response.use(
|
||||
(response) => response,
|
||||
async (error: AxiosError) => {
|
||||
const request = error.config as RetryableRequest | undefined
|
||||
const publicAuthPaths = ['/auth/login', '/auth/register', '/auth/refresh']
|
||||
const isPublicAuthRequest = publicAuthPaths.some((path) => request?.url?.endsWith(path))
|
||||
if (error.response?.status !== 401 || !request || request._retriedAfterRefresh || isPublicAuthRequest) {
|
||||
throw mapError(error)
|
||||
}
|
||||
|
||||
request._retriedAfterRefresh = true
|
||||
try {
|
||||
const tokens = await refreshSession()
|
||||
request.headers.Authorization = `Bearer ${tokens.accessToken}`
|
||||
return await transport.request(request)
|
||||
} catch (refreshError) {
|
||||
clearSessionTokens()
|
||||
window.dispatchEvent(new CustomEvent('cc-client-session-expired'))
|
||||
throw mapError(refreshError)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
export async function apiRequest<T>(config: AxiosRequestConfig) {
|
||||
try {
|
||||
const response = await transport.request<ApiEnvelope<T>>(config)
|
||||
return unwrap(response.data)
|
||||
} catch (error) {
|
||||
throw mapError(error)
|
||||
}
|
||||
}
|
||||
|
||||
export async function apiBlobRequest(config: AxiosRequestConfig) {
|
||||
try {
|
||||
const response = await transport.request<Blob>({ ...config, responseType: 'blob' })
|
||||
return response.data
|
||||
} catch (error) {
|
||||
throw mapError(error)
|
||||
}
|
||||
}
|
||||
|
||||
export { ApiError }
|
||||
@@ -0,0 +1,40 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import test from 'node:test'
|
||||
|
||||
const source = readFileSync(new URL('./merchantIam.ts', import.meta.url), 'utf8')
|
||||
const storeSource = readFileSync(new URL('../stores/tenantAdministration.ts', import.meta.url), 'utf8')
|
||||
|
||||
test('declares the documented merchant role and member endpoints', () => {
|
||||
assert.match(source, /url: '\/merchant\/v1\/iam\/roles'/)
|
||||
assert.match(source, /url: '\/merchant\/v1\/iam\/roles\/permissions'/)
|
||||
assert.match(source, /url: '\/merchant\/v1\/iam\/members'/)
|
||||
assert.match(source, /url: '\/merchant\/v1\/iam\/members\/roles'/)
|
||||
assert.match(source, /url: `\/merchant\/v1\/iam\/members\/\$\{membershipId\}`/)
|
||||
assert.match(source, /url: `\/merchant\/v1\/iam\/members\/\$\{membershipId\}\/status`/)
|
||||
assert.match(source, /url: `\/merchant\/v1\/iam\/members\/\$\{membershipId\}\/password`/)
|
||||
assert.match(source, /url: '\/merchant\/v1\/iam\/profile'/)
|
||||
})
|
||||
|
||||
test('supports tenant profile updates and authenticated qualification downloads', () => {
|
||||
assert.match(source, /method: 'PUT', url: '\/merchant\/v1\/iam\/tenant\/profile'/)
|
||||
assert.match(source, /apiBlobRequest\(\{ method: 'GET', url \}\)/)
|
||||
assert.match(source, /url\.startsWith\('\/merchant\/v1\/files\/qualifications\/'\)/)
|
||||
assert.match(storeSource, /await updateTenantProfile/)
|
||||
})
|
||||
|
||||
test('uses membershipId and full replacement arrays in authorization writes', () => {
|
||||
assert.match(source, /data: \{ roleId, permissionIds \}/)
|
||||
assert.match(source, /data: \{ membershipId, roleIds \}/)
|
||||
assert.doesNotMatch(source, /data: \{ userId, roleIds \}/)
|
||||
})
|
||||
|
||||
test('clears mock records before loading server role and member lists', () => {
|
||||
assert.match(storeSource, /rolesLoadedFromApi\.value = true\s+roles\.value = \[\]/)
|
||||
assert.match(storeSource, /membersLoadedFromApi\.value = true\s+members\.value = \[\]/)
|
||||
assert.match(storeSource, /await createMerchantRole/)
|
||||
assert.match(storeSource, /await createMerchantMember/)
|
||||
assert.match(storeSource, /await updateMerchantMember/)
|
||||
assert.match(storeSource, /await changeMerchantMemberStatus/)
|
||||
assert.match(storeSource, /await resetMerchantMemberPassword/)
|
||||
})
|
||||
@@ -0,0 +1,188 @@
|
||||
import { apiBlobRequest, apiRequest } from '@/api/http'
|
||||
|
||||
export type MerchantTenantStatus = 'PROFILE_REQUIRED' | 'PENDING' | 'ACTIVE' | 'REJECTED' | 'DISABLED'
|
||||
export type QualificationStatus = 'PENDING' | 'APPROVED' | 'REJECTED'
|
||||
|
||||
export interface MerchantTenantProfile {
|
||||
id: number | string
|
||||
tenantCode: string
|
||||
tenantName: string
|
||||
status: MerchantTenantStatus
|
||||
legalName: string
|
||||
unifiedSocialCreditCode: string | null
|
||||
contactName: string
|
||||
contactPhone: string
|
||||
contactEmail: string | null
|
||||
}
|
||||
|
||||
export interface UpdateMerchantTenantProfileRequest {
|
||||
tenantName: string
|
||||
legalName: string
|
||||
contactName: string
|
||||
contactPhone: string
|
||||
contactEmail: string
|
||||
}
|
||||
|
||||
export interface MerchantQualification {
|
||||
id: number | string
|
||||
tenantId: number | string
|
||||
qualificationType: 'BUSINESS_LICENSE'
|
||||
certificateNo: string | null
|
||||
documentUrl: string
|
||||
status: QualificationStatus
|
||||
reviewComment: string | null
|
||||
}
|
||||
|
||||
export interface MerchantPermissionResource {
|
||||
id: number | string
|
||||
tenantId: number | string
|
||||
appCode: 'MERCHANT'
|
||||
permissionType: 'MENU' | 'BUTTON'
|
||||
parentId: number | string
|
||||
permissionCode: string
|
||||
permissionName: string
|
||||
routePath: string | null
|
||||
httpMethod: string | null
|
||||
sortNo: number
|
||||
status: 'ACTIVE' | string
|
||||
}
|
||||
|
||||
export interface QualificationUploadResult {
|
||||
qualificationId: number | string
|
||||
reviewId: number | string
|
||||
tenantStatus: 'PENDING'
|
||||
documentUrl: string
|
||||
}
|
||||
|
||||
export interface MerchantRole {
|
||||
id: number | string
|
||||
tenantId: number | string
|
||||
appCode: 'MERCHANT'
|
||||
roleCode: string
|
||||
roleName: string
|
||||
roleType: 'SYSTEM' | 'CUSTOM' | string
|
||||
status: 'ACTIVE' | 'INACTIVE' | string
|
||||
description: string | null
|
||||
permissionIds: Array<number | string>
|
||||
}
|
||||
|
||||
export interface CreateMerchantRoleRequest {
|
||||
roleCode: string
|
||||
roleName: string
|
||||
description: string
|
||||
}
|
||||
|
||||
export interface MerchantMember {
|
||||
membershipId: number | string
|
||||
userId: number | string
|
||||
authUserId: number | string
|
||||
displayName: string
|
||||
phone: string | null
|
||||
email: string | null
|
||||
memberType: 'OWNER' | string
|
||||
memberNo: string
|
||||
status: 'ACTIVE' | 'INACTIVE' | string
|
||||
roleIds: Array<number | string>
|
||||
}
|
||||
|
||||
export interface MerchantPersonalProfile {
|
||||
userId: number | string
|
||||
authUserId: number | string
|
||||
username: string
|
||||
displayName: string
|
||||
phone: string | null
|
||||
email: string | null
|
||||
}
|
||||
|
||||
export interface CreateMerchantMemberRequest {
|
||||
username: string
|
||||
password: string
|
||||
displayName: string
|
||||
phone: string
|
||||
email: string
|
||||
memberType: 'EMPLOYEE'
|
||||
memberNo: string
|
||||
roleIds: Array<number | string>
|
||||
}
|
||||
|
||||
export interface UpdateMerchantMemberRequest {
|
||||
displayName: string
|
||||
phone: string
|
||||
email: string
|
||||
memberType: string
|
||||
memberNo: string
|
||||
roleIds: Array<number | string>
|
||||
}
|
||||
|
||||
export function getTenantProfile() {
|
||||
return apiRequest<MerchantTenantProfile>({ method: 'GET', url: '/merchant/v1/iam/tenant/profile' })
|
||||
}
|
||||
|
||||
export function updateTenantProfile(payload: UpdateMerchantTenantProfileRequest) {
|
||||
return apiRequest<MerchantTenantProfile>({ method: 'PUT', url: '/merchant/v1/iam/tenant/profile', data: payload })
|
||||
}
|
||||
|
||||
export function getTenantQualifications() {
|
||||
return apiRequest<MerchantQualification[]>({ method: 'GET', url: '/merchant/v1/iam/tenant/qualifications' })
|
||||
}
|
||||
|
||||
export function getMerchantPermissionResources() {
|
||||
return apiRequest<MerchantPermissionResource[]>({ method: 'GET', url: '/merchant/v1/iam/permissions' })
|
||||
}
|
||||
|
||||
export function getMerchantPersonalProfile() {
|
||||
return apiRequest<MerchantPersonalProfile>({ method: 'GET', url: '/merchant/v1/iam/profile' })
|
||||
}
|
||||
|
||||
export function updateMerchantPersonalProfile(payload: Pick<MerchantPersonalProfile, 'displayName' | 'phone' | 'email'>) {
|
||||
return apiRequest<MerchantPersonalProfile>({ method: 'PUT', url: '/merchant/v1/iam/profile', data: payload })
|
||||
}
|
||||
|
||||
export function uploadBusinessLicense(file: File, certificateNo?: string) {
|
||||
const data = new FormData()
|
||||
data.append('file', file)
|
||||
if (certificateNo) data.append('certificateNo', certificateNo)
|
||||
return apiRequest<QualificationUploadResult>({ method: 'POST', url: '/merchant/v1/files/qualifications', data })
|
||||
}
|
||||
|
||||
export function downloadQualificationImage(documentUrl: string) {
|
||||
const url = documentUrl.startsWith('/api/') ? documentUrl.slice(4) : documentUrl
|
||||
if (!url.startsWith('/merchant/v1/files/qualifications/')) throw new Error('资质文件地址无效')
|
||||
return apiBlobRequest({ method: 'GET', url })
|
||||
}
|
||||
|
||||
export function getMerchantRoles() {
|
||||
return apiRequest<MerchantRole[]>({ method: 'GET', url: '/merchant/v1/iam/roles' })
|
||||
}
|
||||
|
||||
export function createMerchantRole(payload: CreateMerchantRoleRequest) {
|
||||
return apiRequest<MerchantRole>({ method: 'POST', url: '/merchant/v1/iam/roles', data: payload })
|
||||
}
|
||||
|
||||
export function replaceMerchantRolePermissions(roleId: number | string, permissionIds: Array<number | string>) {
|
||||
return apiRequest<void>({ method: 'PUT', url: '/merchant/v1/iam/roles/permissions', data: { roleId, permissionIds } })
|
||||
}
|
||||
|
||||
export function getMerchantMembers() {
|
||||
return apiRequest<MerchantMember[]>({ method: 'GET', url: '/merchant/v1/iam/members' })
|
||||
}
|
||||
|
||||
export function createMerchantMember(payload: CreateMerchantMemberRequest) {
|
||||
return apiRequest<MerchantMember>({ method: 'POST', url: '/merchant/v1/iam/members', data: payload })
|
||||
}
|
||||
|
||||
export function updateMerchantMember(membershipId: number | string, payload: UpdateMerchantMemberRequest) {
|
||||
return apiRequest<MerchantMember>({ method: 'PUT', url: `/merchant/v1/iam/members/${membershipId}`, data: payload })
|
||||
}
|
||||
|
||||
export function changeMerchantMemberStatus(membershipId: number | string, status: 'ACTIVE' | 'DISABLED') {
|
||||
return apiRequest<MerchantMember>({ method: 'PUT', url: `/merchant/v1/iam/members/${membershipId}/status`, data: { status } })
|
||||
}
|
||||
|
||||
export function resetMerchantMemberPassword(membershipId: number | string, newPassword: string) {
|
||||
return apiRequest<void>({ method: 'PUT', url: `/merchant/v1/iam/members/${membershipId}/password`, data: { newPassword } })
|
||||
}
|
||||
|
||||
export function replaceMerchantMemberRoles(membershipId: number | string, roleIds: Array<number | string>) {
|
||||
return apiRequest<void>({ method: 'PUT', url: '/merchant/v1/iam/members/roles', data: { membershipId, roleIds } })
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { apiRequest } from '@/api/http'
|
||||
|
||||
export type MerchantPartnerType = 'SENDER' | 'RECEIVER'
|
||||
|
||||
export interface MerchantPartner {
|
||||
id: number | string
|
||||
partnerCode: string
|
||||
partnerName: string
|
||||
partnerType: MerchantPartnerType | string
|
||||
contactName: string | null
|
||||
contactPhone: string | null
|
||||
contactEmail: string | null
|
||||
countryCode: string | null
|
||||
province: string | null
|
||||
city: string | null
|
||||
district: string | null
|
||||
addressLine: string | null
|
||||
postalCode: string | null
|
||||
status: 'ACTIVE' | 'INACTIVE' | 'DISABLED' | string
|
||||
}
|
||||
|
||||
export function getMerchantPartnerCandidates(type: MerchantPartnerType, keyword?: string) {
|
||||
return apiRequest<MerchantPartner[]>({
|
||||
method: 'GET',
|
||||
url: '/merchant/v1/partners/candidates',
|
||||
params: { type, keyword: keyword?.trim() || undefined },
|
||||
})
|
||||
}
|
||||
|
||||
export function getMerchantPartner(id: number | string) {
|
||||
return apiRequest<MerchantPartner>({ method: 'GET', url: `/merchant/v1/partners/${id}` })
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
export const ACCESS_TOKEN_KEY = 'cc-client-access-token'
|
||||
export const REFRESH_TOKEN_KEY = 'cc-client-refresh-token'
|
||||
|
||||
export interface TokenPair {
|
||||
tokenType: string
|
||||
accessToken: string
|
||||
expiresIn: number
|
||||
refreshToken: string
|
||||
refreshExpiresIn: number
|
||||
}
|
||||
|
||||
function activeStorage() {
|
||||
if (localStorage.getItem(ACCESS_TOKEN_KEY)) return localStorage
|
||||
return sessionStorage
|
||||
}
|
||||
|
||||
export function getAccessToken() {
|
||||
return localStorage.getItem(ACCESS_TOKEN_KEY) || sessionStorage.getItem(ACCESS_TOKEN_KEY) || ''
|
||||
}
|
||||
|
||||
export function getRefreshToken() {
|
||||
return localStorage.getItem(REFRESH_TOKEN_KEY) || sessionStorage.getItem(REFRESH_TOKEN_KEY) || ''
|
||||
}
|
||||
|
||||
export function persistTokenPair(tokens: TokenPair, remember?: boolean) {
|
||||
const storage = remember === undefined ? activeStorage() : remember ? localStorage : sessionStorage
|
||||
clearSessionTokens()
|
||||
storage.setItem(ACCESS_TOKEN_KEY, tokens.accessToken)
|
||||
storage.setItem(REFRESH_TOKEN_KEY, tokens.refreshToken)
|
||||
}
|
||||
|
||||
export function clearSessionTokens() {
|
||||
for (const storage of [localStorage, sessionStorage]) {
|
||||
storage.removeItem(ACCESS_TOKEN_KEY)
|
||||
storage.removeItem(REFRESH_TOKEN_KEY)
|
||||
storage.removeItem('cc-client-token')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
export interface ApiEnvelope<T> {
|
||||
code: string
|
||||
message: string
|
||||
data: T
|
||||
requestId: string | null
|
||||
traceId: string | null
|
||||
timestamp: string
|
||||
}
|
||||
|
||||
export type ApiFieldErrors = Record<string, string>
|
||||
|
||||
export class ApiError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
readonly code = 'NETWORK_ERROR',
|
||||
readonly status?: number,
|
||||
readonly fieldErrors?: ApiFieldErrors,
|
||||
readonly traceId?: string | null,
|
||||
) {
|
||||
super(message)
|
||||
this.name = 'ApiError'
|
||||
}
|
||||
}
|
||||
|
||||
export function isApiEnvelope(value: unknown): value is ApiEnvelope<unknown> {
|
||||
if (!value || typeof value !== 'object') return false
|
||||
const record = value as Record<string, unknown>
|
||||
return typeof record.code === 'string' && typeof record.message === 'string' && 'data' in record
|
||||
}
|
||||
|
||||
export function asFieldErrors(value: unknown): ApiFieldErrors | undefined {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined
|
||||
const entries = Object.entries(value).filter((entry): entry is [string, string] => typeof entry[1] === 'string')
|
||||
return entries.length > 0 ? Object.fromEntries(entries) : undefined
|
||||
}
|
||||
Reference in New Issue
Block a user