diff --git a/nginx/default.conf.template b/nginx/default.conf.template index 9063add..bd49fde 100644 --- a/nginx/default.conf.template +++ b/nginx/default.conf.template @@ -13,7 +13,7 @@ server { # 前端统一请求 /api,保持原始路径转发给 Gateway。 location /api/ { - client_max_body_size 6m; + client_max_body_size 12m; proxy_pass ${API_UPSTREAM}; proxy_http_version 1.1; proxy_set_header Host $host; diff --git a/src/deployment/nginxConfig.test.mjs b/src/deployment/nginxConfig.test.mjs index 7fc7ad7..62259a8 100644 --- a/src/deployment/nginxConfig.test.mjs +++ b/src/deployment/nginxConfig.test.mjs @@ -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;/) }) diff --git a/src/modules/tenant-management/api/merchantIam.test.mjs b/src/modules/tenant-management/api/merchantIam.test.mjs index c2f8daf..2ea83d8 100644 --- a/src/modules/tenant-management/api/merchantIam.test.mjs +++ b/src/modules/tenant-management/api/merchantIam.test.mjs @@ -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 \}/) diff --git a/src/modules/tenant-management/api/merchantIam.ts b/src/modules/tenant-management/api/merchantIam.ts index 63d9c29..a033a2b 100644 --- a/src/modules/tenant-management/api/merchantIam.ts +++ b/src/modules/tenant-management/api/merchantIam.ts @@ -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({ + method: 'POST', + url: '/merchant/v1/files/qualifications/required-package', + params: { unifiedSocialCreditCode, replaceBusinessLicense, replaceLegalPersonId }, + data, + }) +} + export function submitTenantQualificationReview() { return apiRequest({ method: 'POST', url: '/merchant/v1/iam/tenant/qualifications/submit-review' }) } diff --git a/src/modules/tenant-management/views/CompanyProfileView.vue b/src/modules/tenant-management/views/CompanyProfileView.vue index 74f9535..a28664b 100644 --- a/src/modules/tenant-management/views/CompanyProfileView.vue +++ b/src/modules/tenant-management/views/CompanyProfileView.vue @@ -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) diff --git a/src/modules/tenant-management/views/TenantAdministrationViews.test.mjs b/src/modules/tenant-management/views/TenantAdministrationViews.test.mjs index e92de85..598fd6b 100644 --- a/src/modules/tenant-management/views/TenantAdministrationViews.test.mjs +++ b/src/modules/tenant-management/views/TenantAdministrationViews.test.mjs @@ -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/)