This commit is contained in:
2026-08-05 09:11:41 +08:00
parent 2313fd238c
commit a6828df5b9
6 changed files with 90 additions and 13 deletions
+1 -1
View File
@@ -5,5 +5,5 @@ import test from 'node:test'
const config = readFileSync(new URL('../../nginx/default.conf.template', import.meta.url), 'utf8')
test('allows qualification uploads with multipart overhead through the API proxy', () => {
assert.match(config, /location \/api\/ \{[\s\S]*?client_max_body_size 6m;/)
assert.match(config, /location \/api\/ \{[\s\S]*?client_max_body_size 12m;/)
})
@@ -34,6 +34,13 @@ test('supports tenant profile updates and authenticated qualification downloads'
assert.match(storeSource, /normalizeQualificationStatus\(item\.status\)/)
})
test('uploads the three required qualification fields through one multipart package request', () => {
assert.match(source, /url: '\/merchant\/v1\/files\/qualifications\/required-package'/)
assert.match(source, /data\.append\('businessLicense', businessLicense\)/)
assert.match(source, /data\.append\('legalPersonId', legalPersonId\)/)
assert.match(source, /params: \{ unifiedSocialCreditCode, replaceBusinessLicense, replaceLegalPersonId \}/)
})
test('uses membershipId and full replacement arrays in authorization writes', () => {
assert.match(source, /data: \{ roleId, permissionIds \}/)
assert.match(source, /data: \{ membershipId, roleIds \}/)
@@ -56,6 +56,14 @@ export interface QualificationUploadResult {
documentUrl: string
}
export interface RequiredQualificationPackageUploadResult {
businessLicenseQualificationId: number | string
legalPersonIdQualificationId: number | string
tenantStatus: MerchantTenantStatus
businessLicenseDocumentUrl: string
legalPersonIdDocumentUrl: string
}
export interface MerchantRole {
id: number | string
tenantId: number | string
@@ -161,6 +169,24 @@ export function uploadMerchantQualification(
})
}
export function uploadMerchantRequiredQualificationPackage(
businessLicense: File,
legalPersonId: File,
unifiedSocialCreditCode: string,
replaceBusinessLicense = false,
replaceLegalPersonId = false,
) {
const data = new FormData()
data.append('businessLicense', businessLicense)
data.append('legalPersonId', legalPersonId)
return apiRequest<RequiredQualificationPackageUploadResult>({
method: 'POST',
url: '/merchant/v1/files/qualifications/required-package',
params: { unifiedSocialCreditCode, replaceBusinessLicense, replaceLegalPersonId },
data,
})
}
export function submitTenantQualificationReview() {
return apiRequest<MerchantTenantProfile>({ method: 'POST', url: '/merchant/v1/iam/tenant/qualifications/submit-review' })
}
@@ -12,6 +12,7 @@ import {
downloadQualificationImage,
submitTenantQualificationReview,
uploadMerchantQualification,
uploadMerchantRequiredQualificationPackage,
type MerchantQualificationType,
} from '@/modules/tenant-management/api/merchantIam'
@@ -80,11 +81,15 @@ const companyCreditCodeValid = computed(() => unifiedSocialCreditCodePattern.tes
const normalizedQualificationCertificateNo = computed(() => qualificationCertificateNo.value.trim().toUpperCase())
const draftCreditCodeValid = computed(() => unifiedSocialCreditCodePattern.test(normalizedQualificationCertificateNo.value))
const businessLicenseFileSelected = computed(() => Boolean(qualificationDraftItem('BUSINESS_LICENSE')?.file))
const qualificationCreditCodeReady = computed(() => companyCreditCodeValid.value
|| (businessLicenseFileSelected.value && draftCreditCodeValid.value))
const legalPersonIdFileSelected = computed(() => Boolean(qualificationDraftItem('LEGAL_PERSON_ID')?.file))
const requiredPackageChangeSelected = computed(() => businessLicenseFileSelected.value || legalPersonIdFileSelected.value)
const requiredPackageFilesSelected = computed(() => businessLicenseFileSelected.value && legalPersonIdFileSelected.value)
const qualificationCreditCodeReady = computed(() => requiredPackageChangeSelected.value
? requiredPackageFilesSelected.value && (companyCreditCodeValid.value || draftCreditCodeValid.value)
: companyCreditCodeValid.value)
const qualificationCreditCodeError = computed(() => {
if (qualificationCreditCodeReady.value) return ''
if (draftCreditCodeValid.value && !businessLicenseFileSelected.value) return '统一社会信用代码必须随营业执照文件共同保存'
if (draftCreditCodeValid.value && !requiredPackageFilesSelected.value) return '统一社会信用代码必须随营业执照和法人身份证共同保存'
return '请输入18位字母或数字组成的统一社会信用代码'
})
const canUploadQualification = computed(() => ['PROFILE_REQUIRED', 'REJECTED'].includes(store.company.auditStatus))
@@ -92,13 +97,15 @@ const uploadedQualificationTypes = computed(() => new Set(store.qualifications
.filter((item) => item.status === 'PENDING')
.map((item) => qualificationTypes.find((type) => type.label === item.type)?.value)
.filter((type): type is MerchantQualificationType => Boolean(type))))
const requiredPackageFilesReady = computed(() => requiredQualificationTypes.every((type) => (
uploadedQualificationTypes.value.has(type.value) || Boolean(qualificationDraftItem(type.value)?.file)
)))
const requiredPackageFilesReady = computed(() => requiredPackageChangeSelected.value
? requiredPackageFilesSelected.value
: requiredQualificationTypes.every((type) => uploadedQualificationTypes.value.has(type.value)))
const requiredPackageComplete = computed(() => qualificationCreditCodeReady.value && requiredPackageFilesReady.value)
const missingRequiredPackageItems = computed(() => {
const missing = requiredQualificationTypes
.filter((type) => !uploadedQualificationTypes.value.has(type.value) && !qualificationDraftItem(type.value)?.file)
.filter((type) => requiredPackageChangeSelected.value
? !qualificationDraftItem(type.value)?.file
: !uploadedQualificationTypes.value.has(type.value))
.map((type) => type.label)
if (!qualificationCreditCodeReady.value) missing.splice(1, 0, '18位统一社会信用代码')
return missing
@@ -330,12 +337,46 @@ async function submitQualification() {
ElMessage.warning(`请完整保存必填业务项:${missingRequiredPackageItems.value.join('、')}`)
return
}
const pendingItems = qualificationDrafts.filter((item) => item.file)
const optionalItems = qualificationDrafts.filter((item) => !item.required && item.file)
qualificationSubmitting.value = true
let uploadedCount = 0
const failures: string[] = []
try {
for (const item of pendingItems) {
if (requiredPackageChangeSelected.value) {
const businessLicense = qualificationDraftItem('BUSINESS_LICENSE')
const legalPersonId = qualificationDraftItem('LEGAL_PERSON_ID')
for (const item of [businessLicense, legalPersonId]) {
if (!item) continue
item.error = ''
item.uploadState = 'uploading'
}
try {
await uploadMerchantRequiredQualificationPackage(
businessLicense?.file as File,
legalPersonId?.file as File,
normalizedQualificationCertificateNo.value,
Boolean(replaceableQualification('BUSINESS_LICENSE')),
Boolean(replaceableQualification('LEGAL_PERSON_ID')),
)
for (const item of [businessLicense, legalPersonId]) {
if (!item) continue
releaseQualificationPreview(item)
item.file = null
item.fileName = ''
item.fileSize = 0
item.fileType = ''
item.inputVersion += 1
item.uploadState = 'uploaded'
}
uploadedCount += 2
} catch (error) {
const message = qualificationUploadErrorMessage(error)
if (businessLicense) { businessLicense.error = message; businessLicense.uploadState = 'error' }
if (legalPersonId) { legalPersonId.error = message; legalPersonId.uploadState = 'error' }
failures.push(`必填资质包:${message}`)
}
}
for (const item of failures.length ? [] : optionalItems) {
const file = item.file
if (!file) continue
item.error = ''
@@ -344,7 +385,7 @@ async function submitQualification() {
await uploadMerchantQualification(
file,
item.value,
item.value === 'BUSINESS_LICENSE' ? normalizedQualificationCertificateNo.value : undefined,
undefined,
Boolean(replaceableQualification(item.value)),
)
releaseQualificationPreview(item)
@@ -33,7 +33,10 @@ test('implements company details, contact information and qualification workflow
assert.match(profileSource, /class="qualification-upload-grid"/)
assert.match(profileSource, /v-for="item in qualificationDrafts"/)
assert.match(profileSource, /保存全部资质/)
assert.match(profileSource, /for \(const item of pendingItems\)/)
assert.match(profileSource, /uploadMerchantRequiredQualificationPackage/)
assert.match(profileSource, /const requiredPackageFilesSelected = computed/)
assert.match(profileSource, /item\.uploadState = 'uploading'/)
assert.match(profileSource, /for \(const item of failures\.length \? \[\] : optionalItems\)/)
assert.match(profileSource, /item\.fileName = ''[\s\S]*item\.uploadState = 'uploaded'/)
assert.match(profileSource, /const canSaveQualificationBatch = computed/)
assert.match(profileSource, /const qualificationCreditCodeReady = computed/)