feat(client): align account and order foundations

This commit is contained in:
2026-09-01 19:34:37 +08:00
parent c1e7ba411e
commit 1305ae4669
23 changed files with 602 additions and 258 deletions
@@ -42,11 +42,18 @@ test('supports tenant profile updates and authenticated qualification downloads'
assert.match(storeSource, /enterpriseType: profile\.enterpriseType \?\? ''/)
})
test('uploads the three required qualification fields through one multipart package request', () => {
test('uploads the required license and omits the optional legal-person file when absent', () => {
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 \}/)
assert.match(source, /if \(legalPersonId\) data\.append\('legalPersonId', legalPersonId\)/)
assert.match(source, /\.\.\.\(legalPersonId \? \{ replaceLegalPersonId: Boolean\(replaceLegalPersonId\) \} : \{\}\)/)
assert.match(source, /legalPersonIdQualificationId: number \| string \| null/)
})
test('uses the dedicated login-identifiers endpoint and keeps phone out of ordinary profile updates', () => {
assert.match(source, /url: '\/merchant\/v1\/iam\/profile\/login-identifiers'/)
assert.match(source, /ChangeMerchantLoginIdentifiersRequest/)
assert.match(source, /Pick<MerchantPersonalProfile, 'displayName' \| 'email'>/)
})
test('uses membershipId and full replacement arrays in authorization writes', () => {
@@ -65,10 +65,10 @@ export interface QualificationUploadResult {
export interface RequiredQualificationPackageUploadResult {
businessLicenseQualificationId: number | string
legalPersonIdQualificationId: number | string
legalPersonIdQualificationId: number | string | null
tenantStatus: MerchantTenantStatus
businessLicenseDocumentUrl: string
legalPersonIdDocumentUrl: string
legalPersonIdDocumentUrl: string | null
}
export interface MerchantRole {
@@ -119,6 +119,12 @@ export interface MerchantPersonalProfile {
email: string | null
}
export interface ChangeMerchantLoginIdentifiersRequest {
username: string
phone: string
currentPassword: string
}
export interface CreateMerchantMemberRequest {
username: string
password: string
@@ -163,10 +169,18 @@ export function getMerchantPersonalProfile() {
return apiRequest<MerchantPersonalProfile>({ method: 'GET', url: '/merchant/v1/iam/profile' })
}
export function updateMerchantPersonalProfile(payload: Pick<MerchantPersonalProfile, 'displayName' | 'phone' | 'email'>) {
export function updateMerchantPersonalProfile(payload: Pick<MerchantPersonalProfile, 'displayName' | 'email'>) {
return apiRequest<MerchantPersonalProfile>({ method: 'PUT', url: '/merchant/v1/iam/profile', data: payload })
}
export function updateMerchantLoginIdentifiers(payload: ChangeMerchantLoginIdentifiersRequest) {
return apiRequest<MerchantPersonalProfile>({
method: 'PUT',
url: '/merchant/v1/iam/profile/login-identifiers',
data: payload,
})
}
export function uploadMerchantQualification(
file: File,
qualificationType: MerchantQualificationType,
@@ -185,18 +199,22 @@ export function uploadMerchantQualification(
export function uploadMerchantRequiredQualificationPackage(
businessLicense: File,
legalPersonId: File,
legalPersonId: File | undefined,
unifiedSocialCreditCode: string,
replaceBusinessLicense = false,
replaceLegalPersonId = false,
replaceLegalPersonId?: boolean,
) {
const data = new FormData()
data.append('businessLicense', businessLicense)
data.append('legalPersonId', legalPersonId)
if (legalPersonId) data.append('legalPersonId', legalPersonId)
return apiRequest<RequiredQualificationPackageUploadResult>({
method: 'POST',
url: '/merchant/v1/files/qualifications/required-package',
params: { unifiedSocialCreditCode, replaceBusinessLicense, replaceLegalPersonId },
params: {
unifiedSocialCreditCode,
replaceBusinessLicense,
...(legalPersonId ? { replaceLegalPersonId: Boolean(replaceLegalPersonId) } : {}),
},
data,
})
}
+2
View File
@@ -3,7 +3,9 @@ export {
getCurrentMerchantPermissionCodes,
getMerchantPersonalProfile,
getTenantProfile,
updateMerchantLoginIdentifiers,
updateMerchantPersonalProfile,
type ChangeMerchantLoginIdentifiersRequest,
type MerchantPermissionResource,
} from './api/merchantIam'
export {
@@ -20,7 +20,7 @@ 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: '请根据平台反馈替换对应资质,必填文件齐全后可重新提交审核。' },
@@ -60,7 +60,7 @@ type QualificationDraftItem = QualificationTypeDefinition & {
const qualificationTypes: QualificationTypeDefinition[] = [
{ value: 'BUSINESS_LICENSE', label: '营业执照', description: '上传清晰完整的营业执照文件。', required: true },
{ value: 'LEGAL_PERSON_ID', label: '法人身份证', description: '正反面请合并为一个 PDF、JPG 或 PNG 文件。', required: true },
{ value: 'LEGAL_PERSON_ID', label: '法人身份证', description: '选填;如上传,正反面请合并为一个 PDF、JPG 或 PNG 文件。', required: false },
{ value: 'FREIGHT_FORWARDER_CERTIFICATE', label: '货代资质证书', description: '如有,可上传当前有效的货代资质证明。', required: false },
{ value: 'BANK_ACCOUNT_LICENSE', label: '银行开户许可证', description: '如有,可上传企业对公账户开户证明。', required: false },
]
@@ -83,14 +83,15 @@ const normalizedQualificationCertificateNo = computed(() => qualificationCertifi
const draftCreditCodeValid = computed(() => unifiedSocialCreditCodePattern.test(normalizedQualificationCertificateNo.value))
const businessLicenseFileSelected = computed(() => Boolean(qualificationDraftItem('BUSINESS_LICENSE')?.file))
const legalPersonIdFileSelected = computed(() => Boolean(qualificationDraftItem('LEGAL_PERSON_ID')?.file))
const requiredPackageChangeSelected = computed(() => businessLicenseFileSelected.value || legalPersonIdFileSelected.value)
const requiredPackageFilesSelected = computed(() => businessLicenseFileSelected.value && legalPersonIdFileSelected.value)
const requiredPackageChangeSelected = computed(() => businessLicenseFileSelected.value)
const requiredPackageFilesSelected = computed(() => businessLicenseFileSelected.value)
const qualificationCreditCodeReady = computed(() => requiredPackageChangeSelected.value
? requiredPackageFilesSelected.value && (companyCreditCodeValid.value || draftCreditCodeValid.value)
: companyCreditCodeValid.value)
const qualificationCreditCodeError = computed(() => {
if (qualificationCreditCodeReady.value) return ''
if (draftCreditCodeValid.value && !requiredPackageFilesSelected.value) return '统一社会信用代码必须随营业执照和法人身份证共同保存'
if (legalPersonIdFileSelected.value && !businessLicenseFileSelected.value) return '法人身份证需与本次营业执照一起保存'
if (draftCreditCodeValid.value && !requiredPackageFilesSelected.value) return '统一社会信用代码必须随营业执照共同保存'
return '请输入18位字母或数字组成的统一社会信用代码'
})
const canUploadQualification = computed(() => ['PROFILE_REQUIRED', 'REJECTED'].includes(store.company.auditStatus))
@@ -121,9 +122,11 @@ const qualificationUploadDisabledReason = computed(() => {
const selectedQualificationCount = computed(() => qualificationDrafts.filter((item) => Boolean(item.file)).length)
const canSaveQualificationBatch = computed(() => canUploadQualification.value
&& selectedQualificationCount.value > 0
&& (!legalPersonIdFileSelected.value || businessLicenseFileSelected.value)
&& requiredPackageComplete.value)
const qualificationSaveDisabledReason = computed(() => {
if (!canUploadQualification.value) return qualificationUploadDisabledReason.value
if (legalPersonIdFileSelected.value && !businessLicenseFileSelected.value) return '法人身份证需与本次营业执照一起保存'
if (!requiredPackageComplete.value) return `请完整保存必填业务项:${missingRequiredPackageItems.value.join('、')}`
if (!selectedQualificationCount.value) return '当前没有需要保存的变更'
return ''
@@ -141,7 +144,7 @@ const qualificationReviewDisabledReason = computed(() => {
return missing.length ? `请先上传:${missing.join('、')}` : ''
})
const qualificationPanelHint = computed(() => canUploadQualification.value
? '营业执照、法人身份证为必填;货代资质证书、银行开户许可证可选填。文件保存后需另行提交审核。'
? '营业执照为必填;法人身份证、货代资质证书、银行开户许可证可选填。文件保存后需另行提交审核。'
: qualificationUploadDisabledReason.value)
const profileFormRules: FormRules<TenantCompanyProfile> = {
companyName: [{ required: true, whitespace: true, message: '请输入公司名称', trigger: 'blur' }],
@@ -340,7 +343,8 @@ async function submitQualification() {
ElMessage.warning(`请完整保存必填业务项:${missingRequiredPackageItems.value.join('、')}`)
return
}
const optionalItems = qualificationDrafts.filter((item) => !item.required && item.file)
const optionalItems = qualificationDrafts.filter((item) => !item.required && item.value !== 'LEGAL_PERSON_ID' && item.file)
const includedLegalPersonId = legalPersonIdFileSelected.value
qualificationSubmitting.value = true
let uploadedCount = 0
const failures: string[] = []
@@ -348,7 +352,7 @@ async function submitQualification() {
if (requiredPackageChangeSelected.value) {
const businessLicense = qualificationDraftItem('BUSINESS_LICENSE')
const legalPersonId = qualificationDraftItem('LEGAL_PERSON_ID')
for (const item of [businessLicense, legalPersonId]) {
for (const item of [businessLicense, ...(legalPersonId?.file ? [legalPersonId] : [])]) {
if (!item) continue
item.error = ''
item.uploadState = 'uploading'
@@ -356,12 +360,13 @@ async function submitQualification() {
try {
await uploadMerchantRequiredQualificationPackage(
businessLicense?.file as File,
legalPersonId?.file as File,
legalPersonId?.file || undefined,
normalizedQualificationCertificateNo.value,
Boolean(replaceableQualification('BUSINESS_LICENSE')),
Boolean(replaceableQualification('LEGAL_PERSON_ID')),
legalPersonId?.file ? Boolean(replaceableQualification('LEGAL_PERSON_ID')) : undefined,
)
for (const item of [businessLicense, legalPersonId]) {
const uploadedRequiredItems = [businessLicense, ...(legalPersonId?.file ? [legalPersonId] : [])]
for (const item of uploadedRequiredItems) {
if (!item) continue
releaseQualificationPreview(item)
item.file = null
@@ -371,11 +376,11 @@ async function submitQualification() {
item.inputVersion += 1
item.uploadState = 'uploaded'
}
uploadedCount += 2
uploadedCount += uploadedRequiredItems.filter(Boolean).length
} catch (error) {
const message = qualificationUploadErrorMessage(error)
if (businessLicense) { businessLicense.error = message; businessLicense.uploadState = 'error' }
if (legalPersonId) { legalPersonId.error = message; legalPersonId.uploadState = 'error' }
if (legalPersonId?.file) { legalPersonId.error = message; legalPersonId.uploadState = 'error' }
failures.push(`必填资质包:${message}`)
}
}
@@ -414,7 +419,9 @@ async function submitQualification() {
return
}
qualificationOpen.value = false
if (refreshed) ElMessage.success('营业执照、统一社会信用代码和法人身份证已完整保存,尚未提交审核')
if (refreshed) ElMessage.success(includedLegalPersonId
? '营业执照、统一社会信用代码和法人身份证已保存,尚未提交审核'
: '营业执照和统一社会信用代码已保存,尚未提交审核')
else ElMessage.warning('资质文件已保存,但最新状态刷新失败,请点击“重新加载”确认状态')
} finally {
qualificationSubmitting.value = false
@@ -582,7 +589,7 @@ function closeQualificationPreview() {
>
<div class="qualification-dialog-intro">
<strong>企业认证必填业务项</strong>
<span>营业执照、18位统一社会信用代码、法人身份证需共同完整保存;其余两项可按需上传。</span>
<span>营业执照和18位统一社会信用代码需共同保存;法人身份证及其他资质可按需上传。</span>
</div>
<div class="qualification-upload-grid">
<article v-for="item in qualificationDrafts" :key="item.value" class="qualification-upload-item">
@@ -51,7 +51,7 @@ test('implements company details, contact information and qualification workflow
assert.match(profileSource, /:close-on-press-escape="!qualificationSubmitting"/)
assert.match(profileSource, /:show-close="!qualificationSubmitting"/)
assert.match(profileSource, /const requiredPackageComplete = computed/)
assert.match(profileSource, /营业执照、18位统一社会信用代码、法人身份证需共同完整保存/)
assert.match(profileSource, /营业执照和18位统一社会信用代码需共同保存;法人身份证及其他资质可按需上传/)
assert.match(profileSource, /已登记的统一社会信用代码不可直接修改/)
assert.match(profileSource, /companyCreditCodeValid\.value/)
assert.doesNotMatch(profileSource, /<el-select v-model="qualificationDraft\.type"/)
@@ -68,13 +68,14 @@ test('implements company details, contact information and qualification workflow
assert.match(profileSource, /const valid = await profileFormRef\.value\?\.validate\(\)/)
assert.match(profileSource, /const requiredQualificationTypes = qualificationTypes\.filter\(\(type\) => type\.required\)/)
assert.match(profileSource, /货代资质证书'.*required: false/)
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, /营业执照、统一社会信用代码和法人身份证已完整保存,尚未提交审核/)
assert.match(profileSource, /营业执照和统一社会信用代码已保存,尚未提交审核/)
assert.match(profileSource, /资质文件已保存,但最新状态刷新失败/)
assert.match(profileSource, /企业资料已提交审核,但最新状态刷新失败/)
assert.match(profileSource, /qualificationStatusMeta/)