优化
This commit is contained in:
@@ -14,7 +14,7 @@ server {
|
|||||||
|
|
||||||
# 前端代码中的 API 前缀为 /api;保持原始路径转给 Gateway。
|
# 前端代码中的 API 前缀为 /api;保持原始路径转给 Gateway。
|
||||||
location /api/ {
|
location /api/ {
|
||||||
client_max_body_size 6m;
|
client_max_body_size 12m;
|
||||||
proxy_pass ${API_UPSTREAM};
|
proxy_pass ${API_UPSTREAM};
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ const config = readFileSync(resolve(process.cwd(), 'nginx/default.conf.template'
|
|||||||
|
|
||||||
describe('admin nginx deployment config', () => {
|
describe('admin nginx deployment config', () => {
|
||||||
it('allows qualification uploads with multipart overhead through the API proxy', () => {
|
it('allows qualification uploads with multipart overhead through the API proxy', () => {
|
||||||
expect(config).toContain('client_max_body_size 6m;')
|
expect(config).toContain('client_max_body_size 12m;')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('prevents the application shell from retaining stale chunk references', () => {
|
it('prevents the application shell from retaining stale chunk references', () => {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
rejectEnterpriseReview,
|
rejectEnterpriseReview,
|
||||||
updatePlatformTenant,
|
updatePlatformTenant,
|
||||||
uploadPlatformTenantQualification,
|
uploadPlatformTenantQualification,
|
||||||
|
uploadPlatformTenantRequiredQualificationPackage,
|
||||||
type EnterpriseReview,
|
type EnterpriseReview,
|
||||||
type PlatformTenantResource,
|
type PlatformTenantResource,
|
||||||
} from '@/modules/iam'
|
} from '@/modules/iam'
|
||||||
@@ -45,6 +46,7 @@ vi.mock('@/modules/iam', () => ({
|
|||||||
submitPlatformTenantReview: vi.fn(),
|
submitPlatformTenantReview: vi.fn(),
|
||||||
updatePlatformTenant: vi.fn(),
|
updatePlatformTenant: vi.fn(),
|
||||||
uploadPlatformTenantQualification: vi.fn(),
|
uploadPlatformTenantQualification: vi.fn(),
|
||||||
|
uploadPlatformTenantRequiredQualificationPackage: vi.fn(),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('@/modules/downloads', () => ({ createPlatformExportTask: vi.fn() }))
|
vi.mock('@/modules/downloads', () => ({ createPlatformExportTask: vi.fn() }))
|
||||||
@@ -107,6 +109,13 @@ describe('CustomersView', () => {
|
|||||||
vi.mocked(uploadPlatformTenantQualification).mockResolvedValue({
|
vi.mocked(uploadPlatformTenantQualification).mockResolvedValue({
|
||||||
qualificationId: 'qualification-1', reviewId: null, tenantStatus: 'PROFILE_REQUIRED', documentUrl: '/files/license.png',
|
qualificationId: 'qualification-1', reviewId: null, tenantStatus: 'PROFILE_REQUIRED', documentUrl: '/files/license.png',
|
||||||
})
|
})
|
||||||
|
vi.mocked(uploadPlatformTenantRequiredQualificationPackage).mockResolvedValue({
|
||||||
|
businessLicenseQualificationId: 'qualification-business',
|
||||||
|
legalPersonIdQualificationId: 'qualification-legal',
|
||||||
|
tenantStatus: 'PROFILE_REQUIRED',
|
||||||
|
businessLicenseDocumentUrl: '/files/license.png',
|
||||||
|
legalPersonIdDocumentUrl: '/files/legal-person-new.png',
|
||||||
|
})
|
||||||
vi.mocked(createPlatformExportTask).mockResolvedValue({
|
vi.mocked(createPlatformExportTask).mockResolvedValue({
|
||||||
id: 'export-1', taskName: '客户列表', taskNo: 'EXP2084', status: 'PENDING',
|
id: 'export-1', taskName: '客户列表', taskNo: 'EXP2084', status: 'PENDING',
|
||||||
} as never)
|
} as never)
|
||||||
@@ -198,7 +207,7 @@ describe('CustomersView', () => {
|
|||||||
expect(getPlatformTenant).not.toHaveBeenCalled()
|
expect(getPlatformTenant).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('saves customer data and selected qualifications with one action', async () => {
|
it('saves both required files and the credit code through one package action', async () => {
|
||||||
vi.mocked(listTenantQualifications).mockResolvedValue([{
|
vi.mocked(listTenantQualifications).mockResolvedValue([{
|
||||||
id: 'qualification-legal', tenantId: fullTenant.id, qualificationType: 'LEGAL_PERSON_ID',
|
id: 'qualification-legal', tenantId: fullTenant.id, qualificationType: 'LEGAL_PERSON_ID',
|
||||||
certificateNo: null, documentUrl: '/files/legal-person.png', status: 'PENDING', reviewComment: null, validTo: null,
|
certificateNo: null, documentUrl: '/files/legal-person.png', status: 'PENDING', reviewComment: null, validTo: null,
|
||||||
@@ -209,9 +218,14 @@ describe('CustomersView', () => {
|
|||||||
await flushPromises()
|
await flushPromises()
|
||||||
|
|
||||||
const licenseFile = new File(['license'], 'license.png', { type: 'image/png' })
|
const licenseFile = new File(['license'], 'license.png', { type: 'image/png' })
|
||||||
const licenseInput = document.querySelector<HTMLInputElement>('.qualification-file input[type="file"]')
|
const legalPersonFile = new File(['legal-person'], 'legal-person.png', { type: 'image/png' })
|
||||||
|
const qualificationInputs = document.querySelectorAll<HTMLInputElement>('.qualification-file input[type="file"]')
|
||||||
|
const licenseInput = qualificationInputs[0]
|
||||||
|
const legalPersonInput = qualificationInputs[1]
|
||||||
Object.defineProperty(licenseInput, 'files', { configurable: true, value: [licenseFile] })
|
Object.defineProperty(licenseInput, 'files', { configurable: true, value: [licenseFile] })
|
||||||
licenseInput?.dispatchEvent(new Event('change', { bubbles: true }))
|
licenseInput?.dispatchEvent(new Event('change', { bubbles: true }))
|
||||||
|
Object.defineProperty(legalPersonInput, 'files', { configurable: true, value: [legalPersonFile] })
|
||||||
|
legalPersonInput?.dispatchEvent(new Event('change', { bubbles: true }))
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
|
|
||||||
const buttons = [...document.querySelectorAll<HTMLButtonElement>('button')]
|
const buttons = [...document.querySelectorAll<HTMLButtonElement>('button')]
|
||||||
@@ -220,14 +234,15 @@ describe('CustomersView', () => {
|
|||||||
await flushPromises()
|
await flushPromises()
|
||||||
|
|
||||||
expect(updatePlatformTenant).toHaveBeenCalledWith(fullTenant.id, expect.objectContaining({ tenantName: fullTenant.tenantName }))
|
expect(updatePlatformTenant).toHaveBeenCalledWith(fullTenant.id, expect.objectContaining({ tenantName: fullTenant.tenantName }))
|
||||||
expect(uploadPlatformTenantQualification).toHaveBeenCalledWith(
|
expect(uploadPlatformTenantRequiredQualificationPackage).toHaveBeenCalledWith(
|
||||||
fullTenant.id,
|
fullTenant.id,
|
||||||
'BUSINESS_LICENSE',
|
|
||||||
licenseFile,
|
licenseFile,
|
||||||
undefined,
|
legalPersonFile,
|
||||||
fullTenant.unifiedSocialCreditCode,
|
fullTenant.unifiedSocialCreditCode,
|
||||||
false,
|
false,
|
||||||
|
true,
|
||||||
)
|
)
|
||||||
|
expect(uploadPlatformTenantQualification).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('submits the current filters to the backend export service without loading every customer page', async () => {
|
it('submits the current filters to the backend export service without loading every customer page', async () => {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
submitPlatformTenantReview,
|
submitPlatformTenantReview,
|
||||||
updatePlatformTenant,
|
updatePlatformTenant,
|
||||||
uploadPlatformTenantQualification,
|
uploadPlatformTenantQualification,
|
||||||
|
uploadPlatformTenantRequiredQualificationPackage,
|
||||||
type CreatePlatformTenantRequest,
|
type CreatePlatformTenantRequest,
|
||||||
type EnterpriseApprovalResponse,
|
type EnterpriseApprovalResponse,
|
||||||
type EnterpriseReview,
|
type EnterpriseReview,
|
||||||
@@ -97,7 +98,8 @@ const queryFields: QueryField[] = [
|
|||||||
] },
|
] },
|
||||||
]
|
]
|
||||||
|
|
||||||
const emptyForm = (): CreatePlatformTenantRequest => ({
|
type CustomerForm = CreatePlatformTenantRequest & { unifiedSocialCreditCode: string }
|
||||||
|
const emptyForm = (): CustomerForm => ({
|
||||||
tenantName: '', legalName: '', legalRepresentative: '', unifiedSocialCreditCode: '', contactName: '', contactPhone: '',
|
tenantName: '', legalName: '', legalRepresentative: '', unifiedSocialCreditCode: '', contactName: '', contactPhone: '',
|
||||||
contactEmail: '', contactWechat: '', servicePackage: '基础版', salesOwnerName: '', paymentTermDays: 30,
|
contactEmail: '', contactWechat: '', servicePackage: '基础版', salesOwnerName: '', paymentTermDays: 30,
|
||||||
enterpriseType: '', establishedDate: null, registeredCapital: 100000, registeredCapitalCurrency: 'CNY',
|
enterpriseType: '', establishedDate: null, registeredCapital: 100000, registeredCapitalCurrency: 'CNY',
|
||||||
@@ -174,17 +176,24 @@ const canSubmitReview = computed(() => Boolean(editingId.value) && ['PROFILE_REQ
|
|||||||
const uploadingQualifications = computed(() => qualifications.value.some((item) => item.uploading))
|
const uploadingQualifications = computed(() => qualifications.value.some((item) => item.uploading))
|
||||||
const hasSelectedQualifications = computed(() => qualifications.value.some((item) => item.file))
|
const hasSelectedQualifications = computed(() => qualifications.value.some((item) => item.file))
|
||||||
const selectedBusinessLicense = computed(() => qualifications.value.find((item) => item.type === 'BUSINESS_LICENSE')?.file)
|
const selectedBusinessLicense = computed(() => qualifications.value.find((item) => item.type === 'BUSINESS_LICENSE')?.file)
|
||||||
const creditCodeReadyForSave = computed(() => creditCodeEstablished.value
|
const selectedLegalPersonId = computed(() => qualifications.value.find((item) => item.type === 'LEGAL_PERSON_ID')?.file)
|
||||||
|| (Boolean(selectedBusinessLicense.value) && unifiedSocialCreditCodePattern.test(form.value.unifiedSocialCreditCode || '')))
|
const requiredPackageChangeSelected = computed(() => Boolean(selectedBusinessLicense.value || selectedLegalPersonId.value))
|
||||||
|
const requiredPackageFilesSelected = computed(() => Boolean(selectedBusinessLicense.value && selectedLegalPersonId.value))
|
||||||
|
const creditCodeReadyForSave = computed(() => requiredPackageChangeSelected.value
|
||||||
|
? requiredPackageFilesSelected.value && (creditCodeEstablished.value
|
||||||
|
|| unifiedSocialCreditCodePattern.test(form.value.unifiedSocialCreditCode || ''))
|
||||||
|
: creditCodeEstablished.value)
|
||||||
const requiredQualificationPackageComplete = computed(() => creditCodeReadyForSave.value
|
const requiredQualificationPackageComplete = computed(() => creditCodeReadyForSave.value
|
||||||
&& qualifications.value.filter((item) => item.required).every((item) => item.uploaded || Boolean(item.file)))
|
&& (requiredPackageChangeSelected.value
|
||||||
|
? requiredPackageFilesSelected.value
|
||||||
|
: qualifications.value.filter((item) => item.required).every((item) => item.uploaded)))
|
||||||
const missingQualificationPackageItems = computed(() => {
|
const missingQualificationPackageItems = computed(() => {
|
||||||
const businessLicense = qualifications.value.find((item) => item.type === 'BUSINESS_LICENSE')
|
const businessLicense = qualifications.value.find((item) => item.type === 'BUSINESS_LICENSE')
|
||||||
const legalPersonId = qualifications.value.find((item) => item.type === 'LEGAL_PERSON_ID')
|
const legalPersonId = qualifications.value.find((item) => item.type === 'LEGAL_PERSON_ID')
|
||||||
const missing: string[] = []
|
const missing: string[] = []
|
||||||
if (!businessLicense?.uploaded && !businessLicense?.file) missing.push('营业执照')
|
if (requiredPackageChangeSelected.value ? !businessLicense?.file : !businessLicense?.uploaded) missing.push('营业执照')
|
||||||
if (!creditCodeReadyForSave.value) missing.push('18位统一社会信用代码(需随营业执照保存)')
|
if (!creditCodeReadyForSave.value) missing.push('18位统一社会信用代码(需随必填资质包保存)')
|
||||||
if (!legalPersonId?.uploaded && !legalPersonId?.file) missing.push('法人身份证')
|
if (requiredPackageChangeSelected.value ? !legalPersonId?.file : !legalPersonId?.uploaded) missing.push('法人身份证')
|
||||||
return missing
|
return missing
|
||||||
})
|
})
|
||||||
const hasUnsavedChanges = computed(() => Boolean(editingId.value) && JSON.stringify(form.value) !== savedFormSnapshot.value)
|
const hasUnsavedChanges = computed(() => Boolean(editingId.value) && JSON.stringify(form.value) !== savedFormSnapshot.value)
|
||||||
@@ -322,7 +331,7 @@ function openCreate() {
|
|||||||
editingId.value = null; editorStatus.value = null; resetEditor(); editorOpen.value = true
|
editingId.value = null; editorStatus.value = null; resetEditor(); editorOpen.value = true
|
||||||
nextTick(() => formRef.value?.clearValidate())
|
nextTick(() => formRef.value?.clearValidate())
|
||||||
}
|
}
|
||||||
function mapTenantToForm(row: PlatformTenantResource): CreatePlatformTenantRequest {
|
function mapTenantToForm(row: PlatformTenantResource): CustomerForm {
|
||||||
return {
|
return {
|
||||||
tenantName: row.tenantName, legalName: row.legalName || '', legalRepresentative: row.legalRepresentative || '', unifiedSocialCreditCode: row.unifiedSocialCreditCode || '',
|
tenantName: row.tenantName, legalName: row.legalName || '', legalRepresentative: row.legalRepresentative || '', unifiedSocialCreditCode: row.unifiedSocialCreditCode || '',
|
||||||
contactName: row.contactName || '', contactPhone: row.contactPhone || '', contactEmail: row.contactEmail || '',
|
contactName: row.contactName || '', contactPhone: row.contactPhone || '', contactEmail: row.contactEmail || '',
|
||||||
@@ -435,16 +444,40 @@ function handleQualificationPreviewError(draft: QualificationDraft) {
|
|||||||
|
|
||||||
async function uploadSelectedQualifications(tenantId: string) {
|
async function uploadSelectedQualifications(tenantId: string) {
|
||||||
const failures: string[] = []
|
const failures: string[] = []
|
||||||
for (const draft of qualifications.value.filter((item) => item.file)) {
|
const businessLicense = qualifications.value.find((item) => item.type === 'BUSINESS_LICENSE')
|
||||||
|
const legalPersonId = qualifications.value.find((item) => item.type === 'LEGAL_PERSON_ID')
|
||||||
|
if (requiredPackageChangeSelected.value && businessLicense?.file && legalPersonId?.file) {
|
||||||
|
businessLicense.uploading = true; legalPersonId.uploading = true
|
||||||
|
businessLicense.error = ''; legalPersonId.error = ''
|
||||||
|
try {
|
||||||
|
const uploaded = await uploadPlatformTenantRequiredQualificationPackage(
|
||||||
|
tenantId, businessLicense.file, legalPersonId.file, form.value.unifiedSocialCreditCode,
|
||||||
|
businessLicense.uploaded, legalPersonId.uploaded,
|
||||||
|
)
|
||||||
|
businessLicense.uploaded = true; legalPersonId.uploaded = true
|
||||||
|
creditCodeEstablished.value = true
|
||||||
|
businessLicense.uploadedName = businessLicense.file.name
|
||||||
|
legalPersonId.uploadedName = legalPersonId.file.name
|
||||||
|
businessLicense.documentUrl = uploaded.businessLicenseDocumentUrl
|
||||||
|
legalPersonId.documentUrl = uploaded.legalPersonIdDocumentUrl
|
||||||
|
businessLicense.file = null; legalPersonId.file = null
|
||||||
|
} catch (error) {
|
||||||
|
const message = getApiErrorMessage(error, '必填资质包上传失败,请重试')
|
||||||
|
businessLicense.error = message; legalPersonId.error = message
|
||||||
|
failures.push('必填资质包')
|
||||||
|
} finally {
|
||||||
|
businessLicense.uploading = false; legalPersonId.uploading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const draft of failures.length ? [] : qualifications.value.filter((item) => !item.required && item.file)) {
|
||||||
draft.uploading = true; draft.error = ''
|
draft.uploading = true; draft.error = ''
|
||||||
try {
|
try {
|
||||||
const uploaded = await uploadPlatformTenantQualification(
|
const uploaded = await uploadPlatformTenantQualification(
|
||||||
tenantId, draft.type, draft.file as File, draft.validTo || undefined,
|
tenantId, draft.type, draft.file as File, draft.validTo || undefined,
|
||||||
draft.type === 'BUSINESS_LICENSE' ? form.value.unifiedSocialCreditCode : undefined,
|
undefined,
|
||||||
draft.uploaded,
|
draft.uploaded,
|
||||||
)
|
)
|
||||||
draft.uploaded = true
|
draft.uploaded = true
|
||||||
if (draft.type === 'BUSINESS_LICENSE') creditCodeEstablished.value = true
|
|
||||||
draft.uploadedName = draft.file?.name || '已登记文件'
|
draft.uploadedName = draft.file?.name || '已登记文件'
|
||||||
draft.documentUrl = uploaded.documentUrl
|
draft.documentUrl = uploaded.documentUrl
|
||||||
draft.file = null
|
draft.file = null
|
||||||
@@ -470,7 +503,8 @@ async function saveCustomer() {
|
|||||||
const { unifiedSocialCreditCode: _immutable, ...payload } = form.value
|
const { unifiedSocialCreditCode: _immutable, ...payload } = form.value
|
||||||
saved = await updatePlatformTenant(editingId.value, payload)
|
saved = await updatePlatformTenant(editingId.value, payload)
|
||||||
} else {
|
} else {
|
||||||
saved = await createPlatformTenant({ ...form.value, unifiedSocialCreditCode: undefined })
|
const { unifiedSocialCreditCode: _qualificationCreditCode, ...payload } = form.value
|
||||||
|
saved = await createPlatformTenant(payload)
|
||||||
editingId.value = String(saved.id)
|
editingId.value = String(saved.id)
|
||||||
}
|
}
|
||||||
editorStatus.value = saved.status
|
editorStatus.value = saved.status
|
||||||
@@ -667,7 +701,7 @@ onMounted(async () => {
|
|||||||
</div><el-form-item label="内部备注" prop="internalRemark"><el-input v-model="form.internalRemark" type="textarea" :rows="3" placeholder="请输入仅平台人员可见的客户备注" maxlength="1000" show-word-limit /></el-form-item></div>
|
</div><el-form-item label="内部备注" prop="internalRemark"><el-input v-model="form.internalRemark" type="textarea" :rows="3" placeholder="请输入仅平台人员可见的客户备注" maxlength="1000" show-word-limit /></el-form-item></div>
|
||||||
|
|
||||||
<div class="form-section qualification-section"><div class="qualification-heading"><div><h3>企业认证必填业务项</h3><p>营业执照、18位统一社会信用代码、法人身份证需共同完整保存;货代资质证书和银行开户许可证选填。</p></div><el-tag v-if="editorStatus" :type="statusMeta(editorStatus).type" effect="light">{{ statusMeta(editorStatus).label }}</el-tag></div>
|
<div class="form-section qualification-section"><div class="qualification-heading"><div><h3>企业认证必填业务项</h3><p>营业执照、18位统一社会信用代码、法人身份证需共同完整保存;货代资质证书和银行开户许可证选填。</p></div><el-tag v-if="editorStatus" :type="statusMeta(editorStatus).type" effect="light">{{ statusMeta(editorStatus).label }}</el-tag></div>
|
||||||
<el-form-item label="统一社会信用代码" prop="unifiedSocialCreditCode" required class="qualification-credit-code"><el-input v-model="form.unifiedSocialCreditCode" :disabled="creditCodeEstablished" placeholder="请输入18位大写字母或数字" maxlength="18" @input="form.unifiedSocialCreditCode = String($event).toUpperCase()" /><small>{{ creditCodeEstablished ? '已登记的统一社会信用代码不可直接修改。' : '必须与营业执照一致,并随营业执照文件共同保存。' }}</small></el-form-item>
|
<el-form-item label="统一社会信用代码" prop="unifiedSocialCreditCode" required class="qualification-credit-code"><el-input v-model="form.unifiedSocialCreditCode" :disabled="creditCodeEstablished" placeholder="请输入18位大写字母或数字" maxlength="18" @input="form.unifiedSocialCreditCode = String($event).toUpperCase()" /><small>{{ creditCodeEstablished ? '已登记的统一社会信用代码不可直接修改。' : '必须与营业执照一致,并随营业执照、法人身份证共同保存。' }}</small></el-form-item>
|
||||||
<div v-if="qualificationLoadError" class="qualification-load-error" role="alert">{{ qualificationLoadError }}<el-button v-if="editingId" link type="primary" @click="loadEditorQualifications(editingId)">重试</el-button></div>
|
<div v-if="qualificationLoadError" class="qualification-load-error" role="alert">{{ qualificationLoadError }}<el-button v-if="editingId" link type="primary" @click="loadEditorQualifications(editingId)">重试</el-button></div>
|
||||||
<div class="qualification-table" role="table" aria-label="客户资质文件">
|
<div class="qualification-table" role="table" aria-label="客户资质文件">
|
||||||
<div class="qualification-row qualification-header" role="row"><span role="columnheader">文件类型</span><span role="columnheader">上传文件</span><span role="columnheader">有效期</span></div>
|
<div class="qualification-row qualification-header" role="row"><span role="columnheader">文件类型</span><span role="columnheader">上传文件</span><span role="columnheader">有效期</span></div>
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import {
|
|||||||
updatePlatformRole,
|
updatePlatformRole,
|
||||||
updatePlatformTenant,
|
updatePlatformTenant,
|
||||||
uploadPlatformTenantQualification,
|
uploadPlatformTenantQualification,
|
||||||
|
uploadPlatformTenantRequiredQualificationPackage,
|
||||||
type PlatformPermissionResource,
|
type PlatformPermissionResource,
|
||||||
type PlatformInternalUser,
|
type PlatformInternalUser,
|
||||||
type PlatformManagedUser,
|
type PlatformManagedUser,
|
||||||
@@ -271,7 +272,7 @@ describe('platform IAM roles', () => {
|
|||||||
it('creates, updates, and changes customer status with the platform tenant contract', async () => {
|
it('creates, updates, and changes customer status with the platform tenant contract', async () => {
|
||||||
const tenant = { id: '8', tenantCode: 'TEN8', tenantName: '柏悦物流', status: 'ACTIVE' }
|
const tenant = { id: '8', tenantCode: 'TEN8', tenantName: '柏悦物流', status: 'ACTIVE' }
|
||||||
const create = {
|
const create = {
|
||||||
tenantName: '柏悦物流', legalName: '柏悦物流有限公司', legalRepresentative: '张三', unifiedSocialCreditCode: '91440300MA5ABCDE12',
|
tenantName: '柏悦物流', legalName: '柏悦物流有限公司', legalRepresentative: '张三',
|
||||||
contactName: '张三', contactPhone: '13800138000', contactEmail: 'ops@example.com', servicePackage: '标准版',
|
contactName: '张三', contactPhone: '13800138000', contactEmail: 'ops@example.com', servicePackage: '标准版',
|
||||||
contactWechat: 'baiyue-ops', salesOwnerName: '李四', paymentTermDays: 30, enterpriseType: '有限责任公司',
|
contactWechat: 'baiyue-ops', salesOwnerName: '李四', paymentTermDays: 30, enterpriseType: '有限责任公司',
|
||||||
establishedDate: '2020-01-01', registeredCapital: 1000, registeredCapitalCurrency: 'CNY',
|
establishedDate: '2020-01-01', registeredCapital: 1000, registeredCapitalCurrency: 'CNY',
|
||||||
@@ -281,12 +282,11 @@ describe('platform IAM roles', () => {
|
|||||||
vi.mocked(http.put).mockResolvedValue({ code: '200', message: 'success', data: tenant })
|
vi.mocked(http.put).mockResolvedValue({ code: '200', message: 'success', data: tenant })
|
||||||
|
|
||||||
await expect(createPlatformTenant(create)).resolves.toEqual(tenant)
|
await expect(createPlatformTenant(create)).resolves.toEqual(tenant)
|
||||||
const { unifiedSocialCreditCode: _creditCode, ...update } = create
|
await expect(updatePlatformTenant('8', create)).resolves.toEqual(tenant)
|
||||||
await expect(updatePlatformTenant('8', update)).resolves.toEqual(tenant)
|
|
||||||
await expect(changePlatformTenantStatus('8', 'DISABLED')).resolves.toEqual(tenant)
|
await expect(changePlatformTenantStatus('8', 'DISABLED')).resolves.toEqual(tenant)
|
||||||
|
|
||||||
expect(http.post).toHaveBeenCalledWith('/platform/v1/iam/tenants', create)
|
expect(http.post).toHaveBeenCalledWith('/platform/v1/iam/tenants', create)
|
||||||
expect(http.put).toHaveBeenNthCalledWith(1, '/platform/v1/iam/tenants/8', update)
|
expect(http.put).toHaveBeenNthCalledWith(1, '/platform/v1/iam/tenants/8', create)
|
||||||
expect(http.put).toHaveBeenNthCalledWith(2, '/platform/v1/iam/tenants/8/status', { status: 'DISABLED' })
|
expect(http.put).toHaveBeenNthCalledWith(2, '/platform/v1/iam/tenants/8/status', { status: 'DISABLED' })
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -297,26 +297,49 @@ describe('platform IAM roles', () => {
|
|||||||
expect(http.get).toHaveBeenCalledWith('/platform/v1/orders/first-leg/tenant-counts?tenantIds=8,9')
|
expect(http.get).toHaveBeenCalledWith('/platform/v1/orders/first-leg/tenant-counts?tenantIds=8,9')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('uploads qualification metadata before explicitly submitting the customer review', async () => {
|
it('uploads optional qualification metadata before explicitly submitting the customer review', async () => {
|
||||||
const uploadResult = { qualificationId: '91', reviewId: null, tenantStatus: 'PROFILE_REQUIRED', documentUrl: '/file.pdf' }
|
const uploadResult = { qualificationId: '91', reviewId: null, tenantStatus: 'PROFILE_REQUIRED', documentUrl: '/file.pdf' }
|
||||||
const tenant = { id: '8', tenantCode: 'TEN8', tenantName: '柏悦物流', status: 'PENDING' }
|
const tenant = { id: '8', tenantCode: 'TEN8', tenantName: '柏悦物流', status: 'PENDING' }
|
||||||
vi.mocked(http.post).mockResolvedValueOnce({ code: '200', message: 'success', data: uploadResult })
|
vi.mocked(http.post).mockResolvedValueOnce({ code: '200', message: 'success', data: uploadResult })
|
||||||
.mockResolvedValueOnce({ code: '200', message: 'success', data: tenant })
|
.mockResolvedValueOnce({ code: '200', message: 'success', data: tenant })
|
||||||
const file = new File(['%PDF-1.4'], '营业执照.pdf', { type: 'application/pdf' })
|
const file = new File(['%PDF-1.4'], '开户许可证.pdf', { type: 'application/pdf' })
|
||||||
|
|
||||||
await expect(uploadPlatformTenantQualification('8', 'BUSINESS_LICENSE', file, '2030-12-31', '91440300MA5ABCDE12', true)).resolves.toEqual(uploadResult)
|
await expect(uploadPlatformTenantQualification('8', 'BANK_ACCOUNT_LICENSE', file, '2030-12-31', undefined, true)).resolves.toEqual(uploadResult)
|
||||||
await expect(submitPlatformTenantReview('8')).resolves.toEqual(tenant)
|
await expect(submitPlatformTenantReview('8')).resolves.toEqual(tenant)
|
||||||
|
|
||||||
const form = vi.mocked(http.post).mock.calls[0]?.[1] as FormData
|
const form = vi.mocked(http.post).mock.calls[0]?.[1] as FormData
|
||||||
expect(http.post).toHaveBeenNthCalledWith(1, '/platform/v1/files/qualifications/8', expect.any(FormData))
|
expect(http.post).toHaveBeenNthCalledWith(1, '/platform/v1/files/qualifications/8', expect.any(FormData))
|
||||||
expect(form.get('file')).toBe(file)
|
expect(form.get('file')).toBe(file)
|
||||||
expect(form.get('qualificationType')).toBe('BUSINESS_LICENSE')
|
expect(form.get('qualificationType')).toBe('BANK_ACCOUNT_LICENSE')
|
||||||
expect(form.get('validTo')).toBe('2030-12-31')
|
expect(form.get('validTo')).toBe('2030-12-31')
|
||||||
expect(form.get('certificateNo')).toBe('91440300MA5ABCDE12')
|
expect(form.get('certificateNo')).toBeNull()
|
||||||
expect(form.get('replace')).toBe('true')
|
expect(form.get('replace')).toBe('true')
|
||||||
expect(http.post).toHaveBeenNthCalledWith(2, '/platform/v1/iam/tenants/8/submit-review', {})
|
expect(http.post).toHaveBeenNthCalledWith(2, '/platform/v1/iam/tenants/8/submit-review', {})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('uploads both required files and the credit code in one package request', async () => {
|
||||||
|
const result = {
|
||||||
|
businessLicenseQualificationId: '91', legalPersonIdQualificationId: '92',
|
||||||
|
tenantStatus: 'PROFILE_REQUIRED', businessLicenseDocumentUrl: '/license.pdf',
|
||||||
|
legalPersonIdDocumentUrl: '/legal-person.png',
|
||||||
|
}
|
||||||
|
vi.mocked(http.post).mockResolvedValue({ code: '200', message: 'success', data: result })
|
||||||
|
const businessLicense = new File(['%PDF-1.4'], '营业执照.pdf', { type: 'application/pdf' })
|
||||||
|
const legalPersonId = new File(['image'], '法人身份证.png', { type: 'image/png' })
|
||||||
|
|
||||||
|
await expect(uploadPlatformTenantRequiredQualificationPackage(
|
||||||
|
'8', businessLicense, legalPersonId, '91440300MA5ABCDE12', true, true,
|
||||||
|
)).resolves.toEqual(result)
|
||||||
|
|
||||||
|
const form = vi.mocked(http.post).mock.calls[0]?.[1] as FormData
|
||||||
|
expect(http.post).toHaveBeenCalledWith('/platform/v1/files/qualifications/8/required-package', form)
|
||||||
|
expect(form.get('businessLicense')).toBe(businessLicense)
|
||||||
|
expect(form.get('legalPersonId')).toBe(legalPersonId)
|
||||||
|
expect(form.get('unifiedSocialCreditCode')).toBe('91440300MA5ABCDE12')
|
||||||
|
expect(form.get('replaceBusinessLicense')).toBe('true')
|
||||||
|
expect(form.get('replaceLegalPersonId')).toBe('true')
|
||||||
|
})
|
||||||
|
|
||||||
it('preserves long review identifiers and returns one-time administrator credentials', async () => {
|
it('preserves long review identifiers and returns one-time administrator credentials', async () => {
|
||||||
const reviewId = '190000000000000001'
|
const reviewId = '190000000000000001'
|
||||||
const approval = {
|
const approval = {
|
||||||
|
|||||||
@@ -53,7 +53,6 @@ export interface CreatePlatformTenantRequest {
|
|||||||
tenantName: string
|
tenantName: string
|
||||||
legalName: string
|
legalName: string
|
||||||
legalRepresentative: string
|
legalRepresentative: string
|
||||||
unifiedSocialCreditCode?: string
|
|
||||||
contactName: string
|
contactName: string
|
||||||
contactPhone: string
|
contactPhone: string
|
||||||
contactEmail: string
|
contactEmail: string
|
||||||
@@ -69,7 +68,7 @@ export interface CreatePlatformTenantRequest {
|
|||||||
internalRemark: string
|
internalRemark: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UpdatePlatformTenantRequest = Omit<CreatePlatformTenantRequest, 'unifiedSocialCreditCode'>
|
export type UpdatePlatformTenantRequest = CreatePlatformTenantRequest
|
||||||
|
|
||||||
export interface PlatformTenantOrderCount {
|
export interface PlatformTenantOrderCount {
|
||||||
tenantId: number | string
|
tenantId: number | string
|
||||||
@@ -112,6 +111,14 @@ export interface PlatformQualificationUploadResponse {
|
|||||||
replacedDocumentUrl?: string | null
|
replacedDocumentUrl?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PlatformRequiredQualificationPackageUploadResponse {
|
||||||
|
businessLicenseQualificationId: number | string
|
||||||
|
legalPersonIdQualificationId: number | string
|
||||||
|
tenantStatus: PlatformTenantStatus
|
||||||
|
businessLicenseDocumentUrl: string
|
||||||
|
legalPersonIdDocumentUrl: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface EnterpriseApprovalResponse {
|
export interface EnterpriseApprovalResponse {
|
||||||
tenantId: number | string
|
tenantId: number | string
|
||||||
administratorCreated: boolean
|
administratorCreated: boolean
|
||||||
@@ -341,6 +348,27 @@ export async function uploadPlatformTenantQualification(
|
|||||||
return unwrapApiResponse(response)
|
return unwrapApiResponse(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function uploadPlatformTenantRequiredQualificationPackage(
|
||||||
|
tenantId: number | string,
|
||||||
|
businessLicense: File,
|
||||||
|
legalPersonId: File,
|
||||||
|
unifiedSocialCreditCode: string,
|
||||||
|
replaceBusinessLicense = false,
|
||||||
|
replaceLegalPersonId = false,
|
||||||
|
) {
|
||||||
|
const form = new FormData()
|
||||||
|
form.append('businessLicense', businessLicense)
|
||||||
|
form.append('legalPersonId', legalPersonId)
|
||||||
|
form.append('unifiedSocialCreditCode', unifiedSocialCreditCode)
|
||||||
|
if (replaceBusinessLicense) form.append('replaceBusinessLicense', 'true')
|
||||||
|
if (replaceLegalPersonId) form.append('replaceLegalPersonId', 'true')
|
||||||
|
const response = await http.post<ApiEnvelope<PlatformRequiredQualificationPackageUploadResponse>, ApiEnvelope<PlatformRequiredQualificationPackageUploadResponse>>(
|
||||||
|
`/platform/v1/files/qualifications/${tenantId}/required-package`,
|
||||||
|
form,
|
||||||
|
)
|
||||||
|
return unwrapApiResponse(response)
|
||||||
|
}
|
||||||
|
|
||||||
export function listPlatformTenantOrderCounts(tenantIds: Array<number | string>) {
|
export function listPlatformTenantOrderCounts(tenantIds: Array<number | string>) {
|
||||||
const ids = tenantIds.map((id) => encodeURIComponent(String(id))).join(',')
|
const ids = tenantIds.map((id) => encodeURIComponent(String(id))).join(',')
|
||||||
return getData<PlatformTenantOrderCount[]>(`/platform/v1/orders/first-leg/tenant-counts?tenantIds=${ids}`)
|
return getData<PlatformTenantOrderCount[]>(`/platform/v1/orders/first-leg/tenant-counts?tenantIds=${ids}`)
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ describe('platform inbound query API', () => {
|
|||||||
it('uses the implemented non-v1 list path and normalizes page values and IDs', async () => {
|
it('uses the implemented non-v1 list path and normalizes page values and IDs', async () => {
|
||||||
vi.mocked(http.get).mockResolvedValue({
|
vi.mocked(http.get).mockResolvedValue({
|
||||||
code: '200', message: 'success', data: {
|
code: '200', message: 'success', data: {
|
||||||
records: [{ id: 19001, orderId: 18001, customerId: 10001, totalPackageCount: '6', holdFlag: 'false' }],
|
records: [{ id: 19001, tenantId: 10002, orderId: 18001, customerId: 10001, totalPackageCount: '6', holdFlag: 'false' }],
|
||||||
page: '1', size: '20', total: '1', pages: '1',
|
page: '1', size: '20', total: '1', pages: '1',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -31,7 +31,7 @@ describe('platform inbound query API', () => {
|
|||||||
await expect(listPlatformInbounds('10001', {
|
await expect(listPlatformInbounds('10001', {
|
||||||
page: 1, size: 20, tab: 'WAIT_MEASURE', inboundCode: 'RK', holdFlag: false,
|
page: 1, size: 20, tab: 'WAIT_MEASURE', inboundCode: 'RK', holdFlag: false,
|
||||||
})).resolves.toMatchObject({
|
})).resolves.toMatchObject({
|
||||||
records: [{ id: '19001', orderId: '18001', customerId: '10001', totalPackageCount: 6, holdFlag: false }],
|
records: [{ id: '19001', tenantId: '10002', orderId: '18001', customerId: '10001', totalPackageCount: 6, holdFlag: false }],
|
||||||
total: 1, pages: 1,
|
total: 1, pages: 1,
|
||||||
})
|
})
|
||||||
expect(http.get).toHaveBeenCalledWith('/platform/inbounds', {
|
expect(http.get).toHaveBeenCalledWith('/platform/inbounds', {
|
||||||
@@ -42,14 +42,14 @@ describe('platform inbound query API', () => {
|
|||||||
it('loads a tenant-scoped aggregate detail and keeps nested IDs as strings', async () => {
|
it('loads a tenant-scoped aggregate detail and keeps nested IDs as strings', async () => {
|
||||||
vi.mocked(http.get).mockResolvedValue({
|
vi.mocked(http.get).mockResolvedValue({
|
||||||
code: '200', message: 'success', data: {
|
code: '200', message: 'success', data: {
|
||||||
summary: { id: 19001, orderId: 18001 }, publicRemark: null,
|
summary: { id: 19001, tenantId: 10002, orderId: 18001 }, publicRemark: null,
|
||||||
palletLocations: [{ palletId: 21001, locationId: 22001 }], files: [{ fileId: 23001, uploadedBy: 2 }],
|
palletLocations: [{ palletId: 21001, locationId: 22001 }], files: [{ fileId: 23001, uploadedBy: 2 }],
|
||||||
internalNotes: [{ id: 24001, createdBy: 2 }], statusHistory: [{ operatorId: 2 }],
|
internalNotes: [{ id: 24001, createdBy: 2 }], statusHistory: [{ operatorId: 2 }],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
await expect(getPlatformInbound('10001', '19001')).resolves.toMatchObject({
|
await expect(getPlatformInbound('10001', '19001')).resolves.toMatchObject({
|
||||||
summary: { id: '19001', orderId: '18001' },
|
summary: { id: '19001', tenantId: '10002', orderId: '18001' },
|
||||||
palletLocations: [{ palletId: '21001', locationId: '22001' }],
|
palletLocations: [{ palletId: '21001', locationId: '22001' }],
|
||||||
files: [{ fileId: '23001', uploadedBy: '2' }],
|
files: [{ fileId: '23001', uploadedBy: '2' }],
|
||||||
internalNotes: [{ id: '24001', createdBy: '2' }],
|
internalNotes: [{ id: '24001', createdBy: '2' }],
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ export function normalizeInboundSummary(raw: InboundOrderSummary): InboundOrderS
|
|||||||
return {
|
return {
|
||||||
...raw,
|
...raw,
|
||||||
id: String(raw.id),
|
id: String(raw.id),
|
||||||
|
tenantId: String(raw.tenantId),
|
||||||
orderId: id(raw.orderId),
|
orderId: id(raw.orderId),
|
||||||
customerId: id(raw.customerId),
|
customerId: id(raw.customerId),
|
||||||
companyId: id(raw.companyId),
|
companyId: id(raw.companyId),
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export type InboundFeeStatus = 'UNPRICED' | 'CALCULATED' | 'CONFIRMED' | 'SENT'
|
|||||||
|
|
||||||
export interface InboundOrderSummary {
|
export interface InboundOrderSummary {
|
||||||
id: string
|
id: string
|
||||||
|
tenantId: string
|
||||||
orderId: string | null
|
orderId: string | null
|
||||||
orderNo: string | null
|
orderNo: string | null
|
||||||
inboundCode: string | null
|
inboundCode: string | null
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ const customers: PlatformTenantResource[] = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
const summary: InboundOrderSummary = {
|
const summary: InboundOrderSummary = {
|
||||||
id: '19001', orderId: '18001', orderNo: 'ORD-001', inboundCode: 'RK-001', processStatus: 'WAIT_MEASURE',
|
id: '19001', tenantId: '10002', orderId: '18001', orderNo: 'ORD-001', inboundCode: 'RK-001', processStatus: 'WAIT_MEASURE',
|
||||||
orderStatusSnapshot: 'APPROVED', measureReviewStatus: 'PENDING', feeStatus: 'UNPRICED', feeReviewStatus: null,
|
orderStatusSnapshot: 'APPROVED', measureReviewStatus: 'PENDING', feeStatus: 'UNPRICED', feeReviewStatus: null,
|
||||||
notificationStatus: null, version: 1, expectedArrivalAt: '2026-08-04T10:00:00', customerId: '10001', customerName: '示例客户',
|
notificationStatus: null, version: 1, expectedArrivalAt: '2026-08-04T10:00:00', customerId: '10001', customerName: '示例客户',
|
||||||
companyId: '10001', companyName: '示例企业', warehouseId: '20001', warehouseName: '义乌收货仓', deliveryWarehouseId: null,
|
companyId: '10001', companyName: '示例企业', warehouseId: '20001', warehouseName: '义乌收货仓', deliveryWarehouseId: null,
|
||||||
@@ -165,7 +165,7 @@ describe('InboundOrdersView', () => {
|
|||||||
expect(listPlatformInbounds).toHaveBeenLastCalledWith('10001', expect.objectContaining({ tab: 'WAIT_REVIEW', page: 1 }))
|
expect(listPlatformInbounds).toHaveBeenLastCalledWith('10001', expect.objectContaining({ tab: 'WAIT_REVIEW', page: 1 }))
|
||||||
;[...document.querySelectorAll<HTMLButtonElement>('button')].filter((button) => button.textContent?.trim() === '查看').at(-1)?.click()
|
;[...document.querySelectorAll<HTMLButtonElement>('button')].filter((button) => button.textContent?.trim() === '查看').at(-1)?.click()
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
expect(getPlatformInbound).toHaveBeenCalledWith('10001', '19001')
|
expect(getPlatformInbound).toHaveBeenCalledWith('10002', '19001')
|
||||||
expect(document.body.textContent).toContain('托盘与库位')
|
expect(document.body.textContent).toContain('托盘与库位')
|
||||||
expect(document.body.textContent).toContain('请优先安排测量')
|
expect(document.body.textContent).toContain('请优先安排测量')
|
||||||
wrapper.unmount()
|
wrapper.unmount()
|
||||||
|
|||||||
@@ -281,7 +281,7 @@ async function openDetail(row: InboundOrderSummary) {
|
|||||||
detailError.value = null
|
detailError.value = null
|
||||||
detail.value = null
|
detail.value = null
|
||||||
try {
|
try {
|
||||||
const response = await getPlatformInbound(recordCustomerId(row), row.id)
|
const response = await getPlatformInbound(row.tenantId, row.id)
|
||||||
if (disposed || requestId !== detailRequestId) return
|
if (disposed || requestId !== detailRequestId) return
|
||||||
detail.value = response
|
detail.value = response
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -293,10 +293,8 @@ async function openDetail(row: InboundOrderSummary) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function closeDetail() { resetDetail() }
|
function closeDetail() { resetDetail() }
|
||||||
function recordCustomerId(row: InboundOrderSummary) { return row.customerId || row.companyId || selectedCustomerId.value }
|
|
||||||
function openWorkspace(row: InboundOrderSummary) {
|
function openWorkspace(row: InboundOrderSummary) {
|
||||||
const customerId = recordCustomerId(row)
|
void router.push({ name: 'inbound-order-detail', params: { inboundId: row.id }, query: { tenantId: row.tenantId } })
|
||||||
void router.push({ name: 'inbound-order-detail', params: { inboundId: row.id }, query: customerId ? { tenantId: customerId } : undefined })
|
|
||||||
}
|
}
|
||||||
function formatNumber(value: number | null, digits = 3) { return value === null ? '—' : value.toFixed(digits) }
|
function formatNumber(value: number | null, digits = 3) { return value === null ? '—' : value.toFixed(digits) }
|
||||||
function formatDate(value: string | null) {
|
function formatDate(value: string | null) {
|
||||||
|
|||||||
Reference in New Issue
Block a user