From d5eaa753291f9436dd7c12a19c86fc6203c045de Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Tue, 4 Aug 2026 14:15:24 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- nginx/default.conf.template | 1 + src/deployment/nginxConfig.test.mjs | 9 +++ src/modules/auth/views/AuthViews.test.mjs | 5 +- src/modules/auth/views/RegisterView.vue | 4 +- .../utils/qualificationFile.test.mjs | 8 ++- .../utils/qualificationFile.ts | 10 ++++ .../views/CompanyProfileView.vue | 60 +++++++------------ .../views/TenantAdministrationViews.test.mjs | 9 ++- 9 files changed, 62 insertions(+), 46 deletions(-) create mode 100644 src/deployment/nginxConfig.test.mjs diff --git a/README.md b/README.md index 22d94ec..0154080 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ pnpm test “我的信息”使用真实个人资料和修改密码接口,姓名、电话、邮箱按服务端约束维护;修改密码成功后清理旧会话并返回登录页。成员列表返回姓名、联系方式和 `roleIds`,新增、编辑、角色全量保存、启停和管理员重置密码均使用真实接口,不回退静态成功。角色页同时加载成员关系,直接打开或刷新后成员数仍按真实 `roleIds` 统计。角色列表返回 `permissionIds`,页面可回显并全量保存角色权限;角色编辑、启停、删除分别调用 `PUT /merchant/v1/iam/roles/{roleId}`、`PUT /merchant/v1/iam/roles/{roleId}/status` 和 `DELETE /merchant/v1/iam/roles/{roleId}`,内置角色及仍有关联成员的角色继续受保护。 -企业资料和资质列表使用真实租户接口,覆盖 `PROFILE_REQUIRED`、`PENDING`、`ACTIVE`、`REJECTED`、`DISABLED` 五种企业状态。企业名称、法定名称和联系人信息支持真实保存;营业执照、法人身份证、货代资质证书和银行开户许可证使用文件服务分别上传,接受 PDF、JPG、PNG 且不超过 5MB,四类齐全后显式提交审核,文件预览通过当前商户 Token 临时下载并在关闭弹窗后释放。 +企业资料和资质列表使用真实租户接口,覆盖 `PROFILE_REQUIRED`、`PENDING`、`ACTIVE`、`REJECTED`、`DISABLED` 五种企业状态。企业名称、法定名称和联系人信息支持真实保存;营业执照、法人身份证为必填资质,货代资质证书和银行开户许可证为选填资质,均使用文件服务分别上传,接受 PDF、JPG、PNG 且不超过 5MB;两项必填资质齐全后可显式提交审核,文件预览通过当前商户 Token 临时下载并在关闭弹窗后释放。 头程订单列表、详情、仓库查询、创建/编辑草稿和提交已经接入真实 API,并显式映射后端 `transportMode`、`containerMode` 和订单状态。创建与提交请求携带幂等键,`merchant:order:create`、`merchant:order:edit`、`merchant:order:submit` 分别映射到独立前端操作权限。页面补齐渠道、提货方式、报关模式、目的仓类型、包装类型等后端必填字段;API 模式先清空本地记录,请求失败不会回退模拟成功。FBA 和海外仓按公共仓类型筛选,选择后由仓库主数据生成目的仓字段,不再要求客户重复选择收货单位;私人仓才填写并按当前用户缓存收件资料。只有“我司提货”需要填写提货联系人和地址,其他送货方式不发送发货方快照,订单也不再依赖往来单位 `customerId`。快递信息仍由平台维护、客户只读。订单取消不属于本轮本地一期验收,客户端不暴露取消写操作。订单产品状态仍按已确认的七状态执行,不设置“已驳回”;若兼容旧接口数据收到 `REJECTED`,只按待审核展示且不开放编辑。 diff --git a/nginx/default.conf.template b/nginx/default.conf.template index 581c6a5..9063add 100644 --- a/nginx/default.conf.template +++ b/nginx/default.conf.template @@ -13,6 +13,7 @@ server { # 前端统一请求 /api,保持原始路径转发给 Gateway。 location /api/ { + client_max_body_size 6m; 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 new file mode 100644 index 0000000..7fc7ad7 --- /dev/null +++ b/src/deployment/nginxConfig.test.mjs @@ -0,0 +1,9 @@ +import assert from 'node:assert/strict' +import { readFileSync } from 'node:fs' +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;/) +}) diff --git a/src/modules/auth/views/AuthViews.test.mjs b/src/modules/auth/views/AuthViews.test.mjs index 1a455dd..4b5f4d3 100644 --- a/src/modules/auth/views/AuthViews.test.mjs +++ b/src/modules/auth/views/AuthViews.test.mjs @@ -42,8 +42,9 @@ test('shows immediate login after registration and separates company review from assert.match(registerSource, /企业账号注册成功/) assert.match(registerSource, /无需等待注册审核/) assert.match(registerSource, /提交订单前还需完成企业审核/) - assert.match(registerSource, /营业执照、法人身份证、货代资质证书和银行开户许可证/) - assert.match(registerSource, /一次补齐四类必填资质并提交审核/) + assert.match(registerSource, /营业执照和法人身份证/) + assert.match(registerSource, /货代资质证书、银行开户许可证可选填/) + assert.match(registerSource, /其他资质可选填/) assert.match(registerSource, /立即登录/) }) diff --git a/src/modules/auth/views/RegisterView.vue b/src/modules/auth/views/RegisterView.vue index 94d68bb..1d63fc0 100644 --- a/src/modules/auth/views/RegisterView.vue +++ b/src/modules/auth/views/RegisterView.vue @@ -116,7 +116,7 @@ async function submit() { 《用户服务协议》 和 《隐私政策》 -

注册成功后账号可立即登录;首次登录需补齐营业执照、法人身份证、货代资质证书和银行开户许可证,提交平台审核通过后才能提交订单。

+

注册成功后账号可立即登录;首次登录需上传营业执照和法人身份证,货代资质证书、银行开户许可证可选填。提交平台审核通过后才能提交订单。

取消注册企业账号
@@ -126,7 +126,7 @@ async function submit() {

企业账号注册成功

{{ form.username }} 现在可以直接登录客户工作台,无需等待注册审核。

企业名称
{{ form.companyName }}
登录账号
{{ form.username }}
注册时间
{{ submittedAt }}
账号状态
可登录
-
提交订单前还需完成企业审核登录后请在“企业资料”一次补齐四类必填资质并提交审核。审核期间仍可维护资料、成员和订单草稿。
+
提交订单前还需完成企业审核登录后请在“企业资料”上传营业执照和法人身份证并提交审核,其他资质可选填。审核期间仍可维护资料、成员和订单草稿。
立即登录
diff --git a/src/modules/tenant-management/utils/qualificationFile.test.mjs b/src/modules/tenant-management/utils/qualificationFile.test.mjs index bc85c40..b145475 100644 --- a/src/modules/tenant-management/utils/qualificationFile.test.mjs +++ b/src/modules/tenant-management/utils/qualificationFile.test.mjs @@ -1,6 +1,6 @@ import assert from 'node:assert/strict' import test from 'node:test' -import { formatQualificationFileSize, QUALIFICATION_MAX_FILE_SIZE, validateQualificationFile } from './qualificationFile.ts' +import { formatQualificationFileSize, qualificationUploadErrorMessage, QUALIFICATION_MAX_FILE_SIZE, validateQualificationFile } from './qualificationFile.ts' test('accepts the backend-confirmed PDF, JPG and PNG formats up to 5MB', () => { assert.equal(validateQualificationFile({ name: '营业执照.jpeg', size: QUALIFICATION_MAX_FILE_SIZE, type: 'image/jpeg' }), '') @@ -20,3 +20,9 @@ test('formats selected file sizes for review', () => { assert.equal(formatQualificationFileSize(1536), '1.5 KB') assert.equal(formatQualificationFileSize(2 * 1024 * 1024), '2.0 MB') }) + +test('turns gateway 413 responses into an actionable upload message', () => { + assert.match(qualificationUploadErrorMessage({ status: 413 }), /上传网关拒绝(413)/) + assert.match(qualificationUploadErrorMessage({ status: 413 }), /已小于 5MB.*检查网关上传限制/) + assert.equal(qualificationUploadErrorMessage(new Error('服务暂不可用')), '服务暂不可用') +}) diff --git a/src/modules/tenant-management/utils/qualificationFile.ts b/src/modules/tenant-management/utils/qualificationFile.ts index 65c1f70..a7a3916 100644 --- a/src/modules/tenant-management/utils/qualificationFile.ts +++ b/src/modules/tenant-management/utils/qualificationFile.ts @@ -23,3 +23,13 @@ export function formatQualificationFileSize(bytes: number) { if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB` return `${(bytes / 1024 / 1024).toFixed(1)} MB` } + +export function qualificationUploadErrorMessage(error: unknown) { + const status = typeof error === 'object' && error !== null && 'status' in error + ? (error as { status?: unknown }).status + : undefined + if (status === 413) { + return '文件被上传网关拒绝(413)。请压缩后重试;若文件已小于 5MB,请联系平台管理员检查网关上传限制' + } + return error instanceof Error ? error.message : '资质文件上传失败' +} diff --git a/src/modules/tenant-management/views/CompanyProfileView.vue b/src/modules/tenant-management/views/CompanyProfileView.vue index 97374b0..bc84525 100644 --- a/src/modules/tenant-management/views/CompanyProfileView.vue +++ b/src/modules/tenant-management/views/CompanyProfileView.vue @@ -6,7 +6,7 @@ import { Building2, CheckCircle2, Clock3, FileCheck2, FileText, Pencil, Trash2, import DataTable, { type DataTableColumn } from '@/components/common/DataTable.vue' import { useTenantAdministrationStore, type TenantCompanyProfile, type TenantQualification } from '@/modules/tenant-management/store/tenantAdministration' import { useAuthStore } from '@/modules/auth/public' -import { formatQualificationFileSize, validateQualificationFile } from '@/modules/tenant-management/utils/qualificationFile' +import { formatQualificationFileSize, qualificationUploadErrorMessage, validateQualificationFile } from '@/modules/tenant-management/utils/qualificationFile' import { qualificationStatusMeta } from '@/modules/tenant-management/utils/qualificationStatus' import { downloadQualificationImage, @@ -19,10 +19,10 @@ const store = useTenantAdministrationStore() const auth = useAuthStore() const companyApproved = computed(() => store.company.auditStatus === 'ACTIVE') const auditStatusCopy = computed(() => ({ - PROFILE_REQUIRED: { title: '请完善企业资质', detail: '四类必填资质提交审核前不能提交头程订单。' }, + PROFILE_REQUIRED: { title: '请完善企业资质', detail: '上传营业执照和法人身份证并提交审核后,才能提交头程订单。' }, PENDING: { title: '企业资料正在等待平台审核', detail: '审核期间仍可维护资料和保存订单草稿;审核通过前不能提交订单。' }, ACTIVE: { title: '企业资料已审核,下单资格已开通', detail: '您可以创建、保存并提交头程订单。' }, - REJECTED: { title: '企业资料审核未通过', detail: '请根据平台反馈替换对应资质,四类文件齐全后可重新提交审核。' }, + REJECTED: { title: '企业资料审核未通过', detail: '请根据平台反馈替换对应资质,必填文件齐全后可重新提交审核。' }, DISABLED: { title: '企业账号已被平台停用', detail: '当前不能提交订单,请联系平台客服核实。' }, })[store.company.auditStatus]) const activeTab = ref('basic') @@ -44,7 +44,7 @@ const loading = ref(false) const loadError = ref('') const draft = reactive({ ...store.company }) type QualificationUploadState = 'idle' | 'uploading' | 'uploaded' | 'error' -type QualificationTypeDefinition = { value: MerchantQualificationType; label: string; description: string } +type QualificationTypeDefinition = { value: MerchantQualificationType; label: string; description: string; required: boolean } type QualificationDraftItem = QualificationTypeDefinition & { file: File | null fileName: string @@ -57,11 +57,12 @@ type QualificationDraftItem = QualificationTypeDefinition & { } const qualificationTypes: QualificationTypeDefinition[] = [ - { value: 'BUSINESS_LICENSE', label: '营业执照', description: '上传清晰完整的营业执照文件。' }, - { value: 'LEGAL_PERSON_ID', label: '法人身份证', description: '正反面请合并为一个 PDF、JPG 或 PNG 文件。' }, - { value: 'FREIGHT_FORWARDER_CERTIFICATE', label: '货代资质证书', description: '上传当前有效的货代资质证明。' }, - { value: 'BANK_ACCOUNT_LICENSE', label: '银行开户许可证', description: '上传企业对公账户开户证明。' }, + { value: 'BUSINESS_LICENSE', label: '营业执照', description: '上传清晰完整的营业执照文件。', required: true }, + { value: 'LEGAL_PERSON_ID', label: '法人身份证', description: '正反面请合并为一个 PDF、JPG 或 PNG 文件。', required: true }, + { value: 'FREIGHT_FORWARDER_CERTIFICATE', label: '货代资质证书', description: '如有,可上传当前有效的货代资质证明。', required: false }, + { value: 'BANK_ACCOUNT_LICENSE', label: '银行开户许可证', description: '如有,可上传企业对公账户开户证明。', required: false }, ] +const requiredQualificationTypes = qualificationTypes.filter((type) => type.required) const qualificationDrafts = reactive(qualificationTypes.map((type) => ({ ...type, file: null, @@ -80,11 +81,11 @@ const normalizedQualificationCertificateNo = computed(() => qualificationCertifi const businessLicenseFileSelected = computed(() => Boolean( qualificationDrafts.find((item) => item.value === 'BUSINESS_LICENSE')?.file, )) -const qualificationCreditCodeReady = computed(() => companyCreditCodeValid.value - || (businessLicenseFileSelected.value && unifiedSocialCreditCodePattern.test(normalizedQualificationCertificateNo.value))) +const qualificationCreditCodeReady = computed(() => !businessLicenseFileSelected.value + || companyCreditCodeValid.value + || unifiedSocialCreditCodePattern.test(normalizedQualificationCertificateNo.value)) const qualificationCreditCodeError = computed(() => { if (qualificationCreditCodeReady.value) return '' - if (!businessLicenseFileSelected.value) return '请重新选择营业执照,并填写18位统一社会信用代码' return '请输入18位字母或数字组成的统一社会信用代码' }) const canUploadQualification = computed(() => ['PROFILE_REQUIRED', 'REJECTED'].includes(store.company.auditStatus)) @@ -98,22 +99,12 @@ const qualificationUploadDisabledReason = computed(() => { if (store.company.auditStatus === 'DISABLED') return '企业账号已停用,暂不能提交资质文件。' return '' }) -const missingQualificationTypes = computed(() => qualificationTypes - .filter((type) => { - const draftItem = qualificationDrafts.find((item) => item.value === type.value) - return !uploadedQualificationTypes.value.has(type.value) - && !draftItem?.file - && draftItem?.uploadState !== 'uploaded' - }) - .map((type) => type.label)) const selectedQualificationCount = computed(() => qualificationDrafts.filter((item) => Boolean(item.file)).length) const canSaveQualificationBatch = computed(() => canUploadQualification.value && selectedQualificationCount.value > 0 - && missingQualificationTypes.value.length === 0 && qualificationCreditCodeReady.value) const qualificationSaveDisabledReason = computed(() => { if (!canUploadQualification.value) return qualificationUploadDisabledReason.value - if (missingQualificationTypes.value.length) return `还需选择:${missingQualificationTypes.value.join('、')}` if (!qualificationCreditCodeReady.value) return qualificationCreditCodeError.value if (!selectedQualificationCount.value) return '请选择需要新增或替换的资质文件' return '' @@ -123,15 +114,15 @@ const qualificationDialogHint = computed(() => qualificationSaveDisabledReason.v : qualificationSaveDisabledReason.value) const canSubmitQualificationReview = computed(() => canUploadQualification.value && companyCreditCodeValid.value - && qualificationTypes.every((type) => uploadedQualificationTypes.value.has(type.value))) + && requiredQualificationTypes.every((type) => uploadedQualificationTypes.value.has(type.value))) const qualificationReviewDisabledReason = computed(() => { if (!canUploadQualification.value) return qualificationUploadDisabledReason.value if (!companyCreditCodeValid.value) return '请先通过营业执照填写18位统一社会信用代码' - const missing = qualificationTypes.filter((type) => !uploadedQualificationTypes.value.has(type.value)).map((type) => type.label) + const missing = requiredQualificationTypes.filter((type) => !uploadedQualificationTypes.value.has(type.value)).map((type) => type.label) return missing.length ? `请先上传:${missing.join('、')}` : '' }) const qualificationPanelHint = computed(() => canUploadQualification.value - ? '一次选择四类必填资质;文件保存不会自动送审,全部齐全后再提交企业审核。' + ? '营业执照、法人身份证为必填;货代资质证书、银行开户许可证可选填。文件保存后需另行提交审核。' : qualificationUploadDisabledReason.value) const profileFormRules: FormRules = { companyName: [{ required: true, whitespace: true, message: '请输入公司名称', trigger: 'blur' }], @@ -243,14 +234,14 @@ function qualificationSlotLabel(item: QualificationDraftItem) { const record = qualificationRecord(item.value) if (record?.status === 'PENDING') return '已上传' if (record?.status === 'REJECTED') return '需重新上传' - return '待上传' + return item.required ? '待上传' : '选填' } function qualificationSlotTagType(item: QualificationDraftItem) { if (item.file) return 'warning' if (item.uploadState === 'uploaded' || qualificationRecord(item.value)?.status === 'PENDING') return 'success' if (item.uploadState === 'error' || qualificationRecord(item.value)?.status === 'REJECTED') return 'danger' - return 'warning' + return item.required ? 'warning' : 'info' } function handleQualificationFileChange(type: MerchantQualificationType, file: UploadFile) { @@ -323,13 +314,6 @@ async function submitQualification() { ElMessage.warning(qualificationUploadDisabledReason.value) return } - if (missingQualificationTypes.value.length) { - qualificationDrafts.forEach((item) => { - if (!uploadedQualificationTypes.value.has(item.value) && !item.file) item.error = '请选择该项资质文件' - }) - ElMessage.warning(`请补齐:${missingQualificationTypes.value.join('、')}`) - return - } if (!qualificationCreditCodeReady.value) { const businessLicense = qualificationDraftItem('BUSINESS_LICENSE') if (businessLicense) businessLicense.error = qualificationCreditCodeError.value @@ -366,7 +350,7 @@ async function submitQualification() { item.uploadState = 'uploaded' uploadedCount += 1 } catch (error) { - const message = error instanceof Error ? error.message : '资质文件上传失败' + const message = qualificationUploadErrorMessage(error) item.error = message item.uploadState = 'error' failures.push(`${item.label}:${message}`) @@ -542,14 +526,14 @@ function closeQualificationPreview() { @closed="resetQualificationDraft" >
- 一次补齐四类必填资质 - 每类选择一个文件后统一保存;文件保存不会自动提交企业审核。 + 必填资质与选填资料 + 营业执照、法人身份证必填;其余两项可按需上传。文件保存不会自动提交企业审核。
- {{ item.label }} + {{ item.label }}({{ item.required ? '必填' : '选填' }})

{{ item.description }}

{{ qualificationSlotLabel(item) }} @@ -579,7 +563,7 @@ function closeQualificationPreview() {
- +

{{ qualificationDialogHint }}

diff --git a/src/modules/tenant-management/views/TenantAdministrationViews.test.mjs b/src/modules/tenant-management/views/TenantAdministrationViews.test.mjs index 6a57884..2838b87 100644 --- a/src/modules/tenant-management/views/TenantAdministrationViews.test.mjs +++ b/src/modules/tenant-management/views/TenantAdministrationViews.test.mjs @@ -47,7 +47,7 @@ test('implements company details, contact information and qualification workflow assert.match(profileSource, /:close-on-click-modal="!qualificationSubmitting"/) assert.match(profileSource, /:close-on-press-escape="!qualificationSubmitting"/) assert.match(profileSource, /:show-close="!qualificationSubmitting"/) - assert.match(profileSource, /请重新选择营业执照,并填写18位统一社会信用代码/) + assert.match(profileSource, /const qualificationCreditCodeReady = computed\(\(\) => !businessLicenseFileSelected\.value/) assert.match(profileSource, /companyCreditCodeValid\.value/) assert.doesNotMatch(profileSource, / type\.required\)/) + assert.match(profileSource, /货代资质证书'.*required: false/) + assert.match(profileSource, /银行开户许可证'.*required: false/) + assert.match(profileSource, /requiredQualificationTypes\.every/) + assert.doesNotMatch(profileSource, /四类必填资质/) + assert.match(profileSource, /qualificationUploadErrorMessage\(error\)/) assert.match(profileSource, /submitTenantQualificationReview/) assert.match(profileSource, /提交企业审核/) assert.match(profileSource, /已保存.*项资质文件,尚未提交审核/)