优化构架

This commit is contained in:
2026-08-03 18:52:21 +08:00
parent 3e347dd7a5
commit 35c713a7ca
51 changed files with 0 additions and 0 deletions
-38
View File
@@ -1,38 +0,0 @@
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 } })
}
-229
View File
@@ -1,229 +0,0 @@
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 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
upcNo?: 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
receivingWarehouseAddress?: string | null
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 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}`
}
-60
View File
@@ -1,60 +0,0 @@
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')
const authSource = readFileSync(new URL('../stores/auth.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, /method: 'PUT', url: `\/merchant\/v1\/iam\/roles\/\$\{roleId\}`/)
assert.match(source, /method: 'PUT', url: `\/merchant\/v1\/iam\/roles\/\$\{roleId\}\/status`/)
assert.match(source, /method: 'DELETE', url: `\/merchant\/v1\/iam\/roles\/\$\{roleId\}`/)
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('starts empty and only loads server role and member records', () => {
assert.match(storeSource, /const members = ref<TenantMember\[]>\(\[\]\)/)
assert.match(storeSource, /const roles = ref<TenantRole\[]>\(\[\]\)/)
assert.match(storeSource, /async function loadRoles\(\) \{\s+roles\.value = \[\]/)
assert.match(storeSource, /async function loadMembers\(\) \{\s+members\.value = \[\]/)
assert.doesNotMatch(storeSource, /initialRoles|initialMembers|示例贸易|演示大道/)
assert.match(storeSource, /await createMerchantRole/)
assert.match(storeSource, /await updateMerchantRole/)
assert.match(storeSource, /await changeMerchantRoleStatus/)
assert.match(storeSource, /await deleteMerchantRole/)
assert.match(storeSource, /await createMerchantMember/)
assert.match(storeSource, /await updateMerchantMember/)
assert.match(storeSource, /await changeMerchantMemberStatus/)
assert.match(storeSource, /await resetMerchantMemberPassword/)
})
test('shares the merchant permission map and preserves resources not rendered by the role editor', () => {
assert.match(storeSource, /export const FRONTEND_TO_MERCHANT_PERMISSION_CODES/)
assert.match(storeSource, /'order:first-leg:cancel': \['merchant:order:cancel'\]/)
assert.match(authSource, /FRONTEND_TO_MERCHANT_PERMISSION_CODES/)
assert.match(authSource, /Object\.entries\(FRONTEND_TO_MERCHANT_PERMISSION_CODES\)/)
assert.match(storeSource, /const preservedPermissionIds = role\.permissionIds\.filter/)
assert.match(storeSource, /const nextPermissionIds = \[\.\.\.new Set\(\[\.\.\.preservedPermissionIds, \.\.\.selectedPermissionIds\]\)\]/)
assert.match(storeSource, /await replaceMerchantRolePermissions\(id, nextPermissionIds\)/)
})
-205
View File
@@ -1,205 +0,0 @@
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 UpdateMerchantRoleRequest {
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 updateMerchantRole(roleId: number | string, payload: UpdateMerchantRoleRequest) {
return apiRequest<MerchantRole>({ method: 'PUT', url: `/merchant/v1/iam/roles/${roleId}`, data: payload })
}
export function changeMerchantRoleStatus(roleId: number | string, status: 'ACTIVE' | 'DISABLED') {
return apiRequest<MerchantRole>({ method: 'PUT', url: `/merchant/v1/iam/roles/${roleId}/status`, data: { status } })
}
export function deleteMerchantRole(roleId: number | string) {
return apiRequest<void>({ method: 'DELETE', url: `/merchant/v1/iam/roles/${roleId}` })
}
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 } })
}
-33
View File
@@ -1,33 +0,0 @@
import { apiRequest } from '@/api/http'
export type MerchantPartnerType = 'CUSTOMER' | 'SUPPLIER' | 'CARRIER' | 'CUSTOMS_BROKER' | 'SHIPPER' | 'CONSIGNEE'
export interface MerchantPartner {
id: number | string
partnerCode: string
partnerName: string
partnerType: MerchantPartnerType | 'BOTH' | string
types?: MerchantPartnerType[]
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}` })
}