Files
hujun e5406c23a6 fix: 修复匹配详情提示及公积金可选路径问题
修复匹配详情中“用户自填未经核验”等来源说明,清理未参与项的橙色注意提示,修正贷款统计口径文案;补充公积金完整可选路径提取逻辑,确保明确无额外时长要求的公积金分支可正确识别并通过。
2026-10-19 16:11:59 +08:00

348 lines
32 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { getSession, requestJson } from '@/services/api.uts'
import { assessmentApiPayload, draftText } from '@/services/assessment.uts'
import { assessmentRequestKey } from '@/services/assessment-flow.uts'
import { contributionLabels, contributionCodes, contributionLabel } from '@/services/contribution.uts'
import { yuanToWan } from '@/services/money.uts'
import { provinceCodeForName } from '@/services/cities.uts'
import { FinancingGradeView, financingGradeFrom } from '@/packages/finance/services/financing-grade.uts'
export type AssessmentRecord = { id: string, type: string, status: string, statusLabel: string, message: string, city: string, submittedAt: string, taskId: string, stage: string, canRetry: boolean, revisionId: string, revisionNo: number, hasReport: boolean, publishedRevisionId: string }
export type AssessmentPage = { items: AssessmentRecord[], hasMore: boolean, nextCursor: string }
export type AssessmentField = { label: string, value: string }
export type ArchiveField = { fieldKey: string, label: string, value: string, isMissing: boolean }
export type ArchiveSection = { id: string, icon: string, title: string, note: string, fields: ArchiveField[], provided: number }
export type AssessmentDetail = { record: AssessmentRecord, fields: ArchiveField[], sections: ArchiveSection[] }
export type ArchiveCompleteness = { assessmentId: string, revisionId: string, percent: number | null, description: string, notApplicableFields: string[] }
const archiveAmountKeys = ['totalDebtBalance', 'monthlyLoanPayment', 'creditCardTotalLimit', 'creditCardUsedLimit']
// 档案只消费报告中已保存的完整度,不用当前表单规则重算旧评估,也不把缺失当作0%。
export async function fetchArchiveCompleteness(id: string, revisionId: string = ''): Promise<ArchiveCompleteness> {
const report = await requestJson('/assessments/' + encodeURIComponent(id) + '/report', 'GET', revisionParams(revisionId))
if (report.getString('assessmentId') != id) throw new Error('报告与当前档案不一致')
if (revisionId.length > 0 && report.getString('revisionId') != revisionId) throw new Error('报告版本不一致,请重新选择。')
const result: ArchiveCompleteness = { assessmentId: id, revisionId: draftText(report, 'revisionId'), percent: null, description: '这份报告尚未评定资料完整度。', notApplicableFields: [] }
if (report.getNumber('resultVersion') == 1) { result.description = '这份旧版报告未计算完整度,已提交资料仍可查看。'; return result }
if (report.getNumber('resultVersion') != 2) throw new Error('报告格式暂不支持')
const completeness = report['completeness'] as UTSJSONObject | null
if (completeness == null || completeness.getString('status') != 'evaluated') return result
const percent = completeness.getNumber('percent')
if (percent == null || !Number.isInteger(percent) || percent < 0 || percent > 100) throw new Error('完整度数据暂不可读取')
result.percent = percent; result.description = '反映本次评估时的资料填写情况,不代表信用评分或审批结果。'
const items = completeness['items'] as UTSJSONObject[] | null
if (items != null && Array.isArray(items)) archiveAmountKeys.forEach((key: string) => {
const matching = items.filter((item: UTSJSONObject): boolean => item != null && item['fieldKey'] == key)
if (matching.length == 1 && matching[0]['state'] == 'not_applicable') result.notApplicableFields.push(key)
})
return result
}
// 只投影同版报告已经保存的不适用项,原档案与编辑回填继续保留原值。
export function archiveSections(detail: AssessmentDetail, completeness: ArchiveCompleteness | null): ArchiveSection[] {
if (completeness == null || completeness.assessmentId != detail.record.id
|| completeness.revisionId.length == 0 || completeness.revisionId != detail.record.revisionId) return detail.sections
return detail.sections.map((section: ArchiveSection): ArchiveSection => ({
id: section.id, icon: section.icon, title: section.title, note: section.note, provided: section.provided,
fields: section.fields.map((field: ArchiveField): ArchiveField => {
if (!field.isMissing || !archiveAmountKeys.includes(field.fieldKey) || !completeness!.notApplicableFields.includes(field.fieldKey)) return field
return { fieldKey: field.fieldKey, label: field.label, value: '不适用', isMissing: field.isMissing }
})
}))
}
function validAssessmentId(value: string): boolean { return /^[1-9][0-9]{0,9}$/.test(value) }
function recordFrom(item: UTSJSONObject, id: string = '', revisionId: string = ''): AssessmentRecord {
const recordId = draftText(item, 'assessmentId'); const revision = draftText(item, 'revisionId')
const revisionNo = item.getNumber('revisionNo')
if (!validAssessmentId(recordId) || !validAssessmentId(revision) || (id.length > 0 && recordId != id) || (revisionId.length > 0 && revision != revisionId)
|| !['personal', 'enterprise'].includes(draftText(item, 'type')) || !['processing', 'failed', 'succeeded'].includes(draftText(item, 'status'))
|| revisionNo == null || !Number.isInteger(revisionNo) || revisionNo < 1
|| (item.getBoolean('hasReport') == true && !validAssessmentId(draftText(item, 'publishedRevisionId')))) throw new Error('评估记录或版本不一致,请重新加载。')
const submitted = draftText(item, 'submittedAt')
const date = new Date(submitted)
const pad = (value: number): string => value < 10 ? '0' + value.toString() : value.toString()
const dateLabel = submitted.length == 0 || isNaN(date.getTime()) ? '时间未知' : date.getFullYear().toString() + '-' + pad(date.getMonth() + 1) + '-' + pad(date.getDate()) + ' ' + pad(date.getHours()) + ':' + pad(date.getMinutes())
return { id: draftText(item, 'assessmentId'), type: draftText(item, 'type'), status: draftText(item, 'status'), statusLabel: draftText(item, 'statusLabel'), message: draftText(item, 'message'), city: draftText(item, 'city'), submittedAt: dateLabel, taskId: draftText(item, 'taskId'), stage: draftText(item, 'stage'), canRetry: item['canRetry'] == true, revisionId: draftText(item, 'revisionId'), revisionNo: item.getNumber('revisionNo') ?? 1, hasReport: item.getBoolean('hasReport') ?? false, publishedRevisionId: draftText(item, 'publishedRevisionId') }
}
export async function retryAssessment(id: string, requestId: string): Promise<void> {
await requestJson('/assessments/' + encodeURIComponent(id) + '/retry', 'POST', { requestId })
}
export type ReportMatching = { status: string, snapshotId: string, ruleVersion: string, orderingVersion: string, eligible: string, ineligible: string, unknown: string }
export type AnalysisReport = { revisionId: string, revisionNo: number, isPreviousResult: boolean, currentStatus: string, version: number, generatedAt: string, completenessPercent: number | null, matching: ReportMatching, summary: string, facts: AssessmentField[], observations: string[], suggestions: string[], priorityItems: string[], dataGaps: string[], disclaimer: string, matchingMessage: string, modelCode: string, demand: AssessmentField[], completenessText: string, completenessItems: AssessmentField[], readinessLabel: string, readinessBasis: string[], warnings: string[], financingGrade: FinancingGradeView }
function reportPriorityItems(improvements: UTSJSONObject[], fields: UTSJSONObject[], basis: UTSJSONObject[]): string[] {
const items: string[] = []; const covered: string[] = []
// 仅用冻结证据中的明确问题标识合并;普通事实引用不表示同一个问题。
improvements.forEach((item: UTSJSONObject) => {
const text = draftText(item, 'text').trim()
if (text.length == 0) return
const keys: string[] = []
const refs = item['evidenceRefs'] as string[]
refs.forEach((ref: string) => {
let related: string[] = []
if (ref.startsWith('fact:')) {
const field = fields.find((entry: UTSJSONObject): boolean => draftText(entry, 'fieldKey') == ref.slice(5))
if (field != null && ['missing', 'invalid'].includes(draftText(field, 'state'))) related = [draftText(field, 'state') + ':' + ref.slice(5)]
} else if (ref.startsWith('readiness:')) {
const entry = basis.find((value: UTSJSONObject): boolean => draftText(value, 'basisId') == ref.slice(10))
if (entry != null && ['MISSING_FIELD', 'INVALID_FIELD'].includes(draftText(entry, 'code'))) {
const kind = draftText(entry, 'code') == 'MISSING_FIELD' ? 'missing' : 'invalid'
related = (entry['fieldRefs'] as string[]).map((key: string): string => kind + ':' + key)
} else if (entry != null && (entry['issueRefs'] as string[]).length > 0) related = [ref]
}
related.forEach((key: string) => { if (!keys.includes(key)) keys.push(key) })
})
if (!items.includes(text) && (keys.length == 0 || keys.some((key: string): boolean => !covered.includes(key)))) items.push(text)
keys.forEach((key: string) => { if (!covered.includes(key)) covered.push(key) })
})
fields.forEach((field: UTSJSONObject) => {
const state = draftText(field, 'state'); const key = state + ':' + draftText(field, 'fieldKey')
const label = draftText(field, 'label').trim()
if (!['missing', 'invalid'].includes(state) || covered.includes(key) || label.length == 0) return
const text = (state == 'missing' ? '补充' : '核实') + label
if (!items.includes(text)) items.push(text)
covered.push(key)
})
return items
}
export async function fetchAnalysisReport(id: string, revisionId: string = ''): Promise<AnalysisReport> {
const result = await requestJson('/assessments/' + encodeURIComponent(id) + '/report', 'GET', revisionParams(revisionId))
const version = result['resultVersion'] as number
const financingGrade = financingGradeFrom(version == 2 ? result['financingGrade'] as UTSJSONObject | null : null)
if (revisionId.length > 0 && result.getString('revisionId') != revisionId) throw new Error('报告版本不一致,请重新选择。')
if (result['assessmentId'] != id || (version != 1 && version != 2) || (version == 1 && result['reportType'] != 'self_reported_analysis') || (version == 2 && result['reportType'] != 'financing_needs_analysis')) throw new Error('报告格式不支持')
const date = new Date(draftText(result, 'generatedAt'))
const pad = (value: number): string => value < 10 ? '0' + value.toString() : value.toString()
const generatedAt = isNaN(date.getTime()) ? '未记录' : date.getFullYear().toString() + '-' + pad(date.getMonth() + 1) + '-' + pad(date.getDate()) + ' ' + pad(date.getHours()) + ':' + pad(date.getMinutes())
const rawMatching = result['matching'] as UTSJSONObject | null
const matching: ReportMatching = { status: 'unavailable', snapshotId: '', ruleVersion: '', orderingVersion: '', eligible: '—', ineligible: '—', unknown: '—' }
if (version == 2 && rawMatching != null) {
matching.status = draftText(rawMatching, 'status')
if (!['completed','unavailable'].includes(matching.status)) throw new Error('报告匹配状态暂不可读取')
if (matching.status == 'completed') {
matching.snapshotId = draftText(rawMatching, 'catalogSnapshotId'); matching.ruleVersion = draftText(rawMatching, 'ruleVersion'); matching.orderingVersion = draftText(rawMatching, 'orderingPolicyVersion')
const count = (key: string): string => { const value = rawMatching!.getNumber(key); if (value == null || !Number.isInteger(value) || value < 0) throw new Error('报告匹配统计暂不可读取'); return value.toString() }
matching.eligible = count('candidateCount'); matching.ineligible = count('ineligibleCount'); matching.unknown = count('unknownCount')
if (matching.snapshotId.length == 0 || matching.ruleVersion.length == 0 || matching.orderingVersion.length == 0) throw new Error('报告匹配版本暂不可读取')
}
}
const facts: AssessmentField[] = []
const rawFacts = result['facts'] as UTSJSONObject[]
rawFacts.forEach((fact: UTSJSONObject) => {
const raw = fact['value']
let value = raw == null ? '未填写' : (typeof raw == 'number' ? (raw as number).toString() : draftText(fact, 'value'))
if (fact['value'] != null && fact['unit'] == 'yuan') value = yuanToWan(value) + ' 万元'
if (fact['value'] != null && fact['unit'] == 'month') value += ' 个月'
if (fact['value'] != null && fact['unit'] == 'age') value += ' 岁'
if (fact['state'] == 'invalid') value = '格式需核实'
const enumLabels: UTSJSONObject = { six_months_plus: '连续缴纳6个月以上', under_six_months: '缴纳不足6个月', none: '无' }
if (['socialSecurity', 'housingFund'].includes(draftText(fact, 'fieldKey'))) value = contributionLabel(value)
if (enumLabels[value] != null) value = enumLabels[value] as string
// v2的需求在独立区块展示,避免用途代码和同一金额重复出现。
if (version == 2 && ['requestedAmountYuan', 'requestedTermMonths', 'purposeCode'].includes(draftText(fact, 'fieldKey'))) return
facts.push({ label: draftText(fact, 'label'), value } as AssessmentField)
})
if (version == 2) {
const demandData = result['demand'] as UTSJSONObject
const completeness = result['completeness'] as UTSJSONObject
const readiness = result['readiness'] as UTSJSONObject
const items = completeness['items'] as UTSJSONObject[]
const basis = readiness['basis'] as UTSJSONObject[]
const stateLabels: UTSJSONObject = { provided: '已填写', missing: '待补充', invalid: '需核实', not_applicable: '不适用' }
const completenessItems = items.map((item: UTSJSONObject): AssessmentField => ({ label: draftText(item, 'label'), value: stateLabels[draftText(item, 'state')] as string }))
const demand: AssessmentField[] = result['inputVersion'] == 3 ? [] : [
{ label: '融资金额', value: yuanToWan(draftText(demandData, 'requestedAmountYuan')) + ' 万元' },
{ label: '融资期限', value: (demandData['requestedTermMonths'] as number).toString() + ' 个月' },
{ label: '资金用途', value: draftText(demandData, 'purposeLabel') }
]
const evaluated = completeness['status'] == 'evaluated'
const completenessPercent = evaluated ? completeness.getNumber('percent') : null
if (evaluated && (completenessPercent == null || !Number.isInteger(completenessPercent) || completenessPercent < 0 || completenessPercent > 100)) throw new Error('完整度数据暂不可读取')
const completenessText = evaluated ? (completeness['percent'] as number).toString() + '% · 已填写 ' + (completeness['providedCount'] as number).toString() + ' / ' + (completeness['applicableCount'] as number).toString() + ' 项' : '资料完整度规则待配置'
// 模型文本仅显示已通过后端验证的证据引用;准备状态不由模型推导。
const explain = (key: string): string[] => (result[key] as UTSJSONObject[]).map((item: UTSJSONObject): string => {
const labels = (item['evidenceRefs'] as string[]).map((ref: string): string => {
if (ref.startsWith('fact:')) {
const fact = rawFacts.find((entry: UTSJSONObject): boolean => draftText(entry, 'fieldKey') == ref.slice(5))
return fact == null ? '' : draftText(fact, 'label')
}
const entry = basis.find((entry: UTSJSONObject): boolean => draftText(entry, 'basisId') == ref.slice(10))
return entry == null ? '' : draftText(entry, 'message')
}).filter((label: string): boolean => label.length > 0)
return draftText(item, 'text') + (labels.length > 0 ? '(依据:' + labels.join(';') + ')' : '')
})
return { revisionId: draftText(result, 'revisionId'), revisionNo: result.getNumber('revisionNo') ?? 1, isPreviousResult: result.getBoolean('isPreviousResult') ?? false, currentStatus: draftText(result, 'currentStatus'), version, generatedAt, completenessPercent, matching, summary: draftText(result, 'summary'), facts, observations: explain('strengths'), suggestions: explain('improvements'), priorityItems: reportPriorityItems(result['improvements'] as UTSJSONObject[], items, basis), dataGaps: explain('riskWarnings'),
// 新报告展示服务端冻结结果的统计;旧报告无此字段时保留原提示,不重算历史。
disclaimer: draftText(result, 'disclaimer'), matchingMessage: draftText(result, 'matchingMessage').length > 0 ? draftText(result, 'matchingMessage') : '产品资料及匹配规则尚未就绪,本报告未执行产品匹配。', modelCode: draftText(result, 'modelCode'),
demand, completenessText, completenessItems, financingGrade, readinessLabel: readiness['status'] == 'evaluated' ? draftText(readiness, 'label') : '未评定',
readinessBasis: basis.map((item: UTSJSONObject): string => draftText(item, 'message')), warnings: result['systemWarnings'] as string[] }
}
const observations = (result['observations'] as UTSJSONObject[]).map((item: UTSJSONObject): string => {
const fact = rawFacts.find((entry: UTSJSONObject): boolean => entry['id'] == item['factId'])
return (fact == null ? '' : draftText(fact, 'label') + ':') + draftText(item, 'text')
})
const legacySuggestions = (result['suggestions'] as string[]).map((text: string): string => text.trim())
return { revisionId: draftText(result, 'revisionId'), revisionNo: result.getNumber('revisionNo') ?? 1, isPreviousResult: result.getBoolean('isPreviousResult') ?? false, currentStatus: draftText(result, 'currentStatus'), version, generatedAt, completenessPercent: null, matching, summary: draftText(result, 'summary'), facts, observations, suggestions: result['suggestions'] as string[], priorityItems: legacySuggestions.filter((text: string, index: number): boolean => text.length > 0 && legacySuggestions.indexOf(text) == index), dataGaps: result['dataGaps'] as string[], disclaimer: draftText(result, 'disclaimer'), matchingMessage: draftText(result, 'matchingMessage'), modelCode: draftText(result, 'modelCode'), demand: [], completenessText: '旧版报告未计算资料完整度', completenessItems: [], readinessLabel: '未评定', readinessBasis: [], warnings: [], financingGrade }
}
function enumValue(value: string, labels: string[], codes: string[]): string | null {
if (value.length == 0) return null
const index = labels.indexOf(value)
if (index < 0) throw new Error('选项已变化,请重新选择')
return codes[index]
}
export async function createAssessment(payload: UTSJSONObject): Promise<string> {
const session = getSession()
if (session == null) throw new Error('请先登录')
const documents = payload['documents'] as UTSJSONObject[] | null
const grants = payload['authorizations'] as string[] | null
if (documents != null && documents.some((item) => !/^[1-9][0-9]{0,18}$/.test(item.getString('documentId') ?? '') || !/^[1-9][0-9]{0,18}$/.test(item.getString('consentId') ?? '') || item.getBoolean('deletePending') == true))
throw new Error('请先完成照片上传或移除未完成资料')
if (grants != null && grants.length > 0) throw new Error('外部查询尚未开放,请取消授权选择后提交')
const editId = draftText(payload, 'editAssessmentId'); const baseRevision = draftText(payload, 'baseRevisionId')
if (editId.length > 0 && (!validAssessmentId(editId) || !validAssessmentId(baseRevision))) throw new Error('修改版本无效,请返回档案重新读取。')
if (!['personal', 'enterprise'].includes(draftText(payload, 'type'))) throw new Error('评估类型无效,请重新填写。')
const values = assessmentApiPayload(payload)
const form = new UTSJSONObject()
const textKeys = ['name', 'idCard', 'phone', 'city', 'occupation', 'company', 'enterpriseName', 'businessCode']
const moneyKeys = ['monthlyIncome', 'annualRevenue', 'totalDebtBalance', 'monthlyLoanPayment', 'creditCardTotalLimit', 'creditCardUsedLimit', 'socialSecurityBase', 'housingFundBase']
textKeys.concat(moneyKeys).forEach((key: string) => { const value = draftText(values, key).trim(); form[key] = value.length == 0 ? null : value })
if (form['idCard'] != null) form['idCard'] = (form['idCard'] as string).toUpperCase()
if (form['businessCode'] != null) form['businessCode'] = (form['businessCode'] as string).toUpperCase()
const countKeys = ['creditLoanCount', 'creditCardCount', 'operatingMonths']
countKeys.forEach((key: string) => {
const value = draftText(values, key)
if (value.length > 0 && !/^[0-9]{1,4}$/.test(value)) throw new Error('数量与经营月数须为0至9999的整数')
form[key] = value.length == 0 ? null : parseInt(value)
})
if (draftText(values, 'loanCountScope') == 'all_loans') form['loanCountScope'] = 'all_loans'
form['maritalStatus'] = enumValue(draftText(values, 'maritalStatus'), ['已婚', '未婚', '离异', '丧偶'], ['married', 'single', 'divorced', 'widowed'])
const socialKeys = ['socialSecurity', 'housingFund']
socialKeys.forEach((key: string) => { form[key] = enumValue(draftText(values, key), contributionLabels, contributionCodes) })
const ownershipKeys = ['house', 'car']
ownershipKeys.forEach((key: string) => { form[key] = enumValue(draftText(values, key), ['有', '无'], ['yes', 'no']) })
const provinceCode = provinceCodeForName(draftText(values, 'city').trim())
const cityNames = ['深圳市', '广州市', '北京市', '上海市', '杭州市']
const cityCodes = ['440300', '440100', '110100', '310100', '330100']
const cityIndex = cityNames.indexOf(draftText(values, 'city'))
form['cityCode'] = provinceCode != null || cityIndex < 0 ? null : cityCodes[cityIndex]
if (provinceCode != null) form['provinceCode'] = provinceCode
// 卡片资料单独保存,确认页明示暂不绑定评估;只发送自填信息,绝不把本机路径或附件送入分析。
const body: UTSJSONObject = { inputVersion: 3, type: values['type'], moneyUnit: 'yuan', form, documents: [] as UTSJSONObject[], consentIds: [] as string[] }
if (editId.length > 0) body['baseRevisionId'] = baseRevision
// 新建和修改使用不同幂等作用域,回执丢失时安全重放原提交。
body['requestId'] = assessmentRequestKey(session['token'] as string, editId + ':' + JSON.stringify(body))
const path = editId.length > 0 ? '/assessments/' + encodeURIComponent(editId) + '/revisions' : '/assessments'
const result = await requestJson(path, 'POST', body)
const id = draftText(result, 'assessmentId')
const revision = draftText(result, 'revisionId')
if (!validAssessmentId(id) || !validAssessmentId(revision) || result.getString('submissionStatus') != 'accepted'
|| (editId.length > 0 && (id != editId || revision == baseRevision))) throw new Error('提交回执不一致,请使用原提交重试。')
return id
}
export async function fetchAssessments(cursor: string, type: string = ''): Promise<AssessmentPage> {
const params: UTSJSONObject = { limit: '20', cursor }
// 按主体在服务端过滤后分页,不能只过滤当前页而漏掉更早的企业/个人档案。
if (type.length > 0) params['type'] = type
const result = await requestJson('/assessments', 'GET', params)
return assessmentPageFrom(result, cursor, type)
}
// 记录和版本分页必须属于当前选择,坏页不能推进游标或拼入另一评估。
function assessmentPageFrom(result: UTSJSONObject, cursor: string, type: string = '', id: string = ''): AssessmentPage {
const rawItems = result['items'] as UTSJSONObject[] | null
const more = result.getBoolean('hasMore'); const next = draftText(result, 'nextCursor')
if (rawItems == null || !Array.isArray(rawItems) || more == null) throw new Error('历史分页格式不完整,请重试。')
const items: AssessmentRecord[] = []; let previous = cursor
rawItems.forEach((item: UTSJSONObject) => {
const record = recordFrom(item, id)
const key = id.length > 0 ? record.revisionId : record.id
if ((type.length > 0 && record.type != type) || (previous.length > 0 && parseInt(key) >= parseInt(previous))) throw new Error('历史分页记录不一致,请重试。')
previous = key; items.push(record)
})
if ((more && (items.length == 0 || !validAssessmentId(next) || next != previous)) || (!more && next.length > 0)) throw new Error('历史分页游标不一致,请重试。')
return { items, hasMore: more, nextCursor: next }
}
export async function fetchAssessmentStatus(id: string): Promise<AssessmentRecord> {
return recordFrom(await requestJson('/assessments/' + encodeURIComponent(id) + '/status', 'GET', {}), id)
}
export async function fetchAssessmentDetail(id: string, revisionId: string = ''): Promise<AssessmentDetail> {
const result = await requestJson('/assessments/' + encodeURIComponent(id), 'GET', revisionParams(revisionId))
if (result.getString('assessmentId') != id) throw new Error('评估详情与当前档案不一致')
if (revisionId.length > 0 && result.getString('revisionId') != revisionId) throw new Error('评估详情版本不一致,请重新选择。')
const record = recordFrom(result, id, revisionId)
const form = result['form'] as UTSJSONObject | null
if (form == null) throw new Error('评估详情不完整')
const fields: ArchiveField[] = []
const enterprise = result['type'] == 'enterprise'
const sections: ArchiveSection[] = [
{ id: 'basic', icon: 'file-text', title: '基本信息', note: enterprise ? '企业与联系人信息' : '身份与联系方式', fields: [], provided: 0 },
{ id: 'income', icon: 'income', title: enterprise ? '经营信息' : '职业与收入', note: enterprise ? '经营行业、月数与营收' : '职业、收入与缴纳情况', fields: [], provided: 0 },
{ id: 'assets', icon: 'asset', title: '资产与负债', note: '贷款、信用卡额度与房车情况', fields: [], provided: 0 },
{ id: 'authorization', icon: 'authorization', title: '资料与授权', note: '私有资料与外部查询授权', fields: [], provided: 0 }
]
const addField = (section: number, fieldKey: string, label: string, value: string, provided: boolean, isMissing: boolean) => {
const field: ArchiveField = { fieldKey, label, value, isMissing }
fields.push(field); sections[section].fields.push(field)
if (provided) sections[section].provided++
}
const keys = ['name', 'phone', 'city', 'idCard', 'enterpriseName', 'businessCode', 'occupation', 'company', 'operatingMonths', 'monthlyIncome', 'annualRevenue', 'creditLoanCount', 'totalDebtBalance', 'monthlyLoanPayment', 'creditCardCount', 'creditCardTotalLimit', 'creditCardUsedLimit', 'maritalStatus', 'socialSecurity', 'housingFund', 'house', 'car', 'socialSecurityBase', 'housingFundBase']
const labels = ['姓名 / 联系人', '手机号', '城市', '身份证号', '企业名称', '信用代码', '职业 / 行业', '工作单位', '经营月数', '月收入', '年营收', '信用贷款笔数', '欠款总余额', '贷款月还款金额', '信用卡张数', '信用卡总额度', '信用卡已用额度', '婚姻状况', '社保缴纳', '公积金缴纳', '是否有房', '是否有车', '社保月缴存基数', '公积金月缴存基数']
labels[11] = draftText(form, 'loanCountScope') == 'all_loans' ? '贷款笔数' : '信用贷款笔数(旧口径)'
const moneyKeys = ['monthlyIncome', 'annualRevenue', 'totalDebtBalance', 'monthlyLoanPayment', 'creditCardTotalLimit', 'creditCardUsedLimit', 'socialSecurityBase', 'housingFundBase']
if (form.getString('provinceCode') != null) labels[2] = '所在省份'
const enumLabels: UTSJSONObject = { married: '已婚', single: '未婚', divorced: '离异', widowed: '丧偶', yes: '有', no: '无', six_months_plus: '连续缴纳6个月以上', under_six_months: '缴纳不足6个月', none: '未缴纳' }
keys.forEach((key: string, index: number) => {
const raw = form[key]
let value = raw == null ? '未填写' : (typeof raw == 'number' ? (raw as number).toString() : raw as string)
if (raw != null && moneyKeys.includes(key)) value = yuanToWan(value) + ' 万元'
if (raw != null && key == 'operatingMonths') value += ' 个月'
if (['socialSecurity', 'housingFund'].includes(key)) value = contributionLabel(value)
if (['maritalStatus', 'socialSecurity', 'housingFund', 'house', 'car'].includes(key) && enumLabels[value] != null) value = enumLabels[value] as string
const personalOnly = ['idCard', 'company', 'monthlyIncome', 'maritalStatus', 'socialSecurity', 'housingFund', 'socialSecurityBase', 'housingFundBase']
const enterpriseOnly = ['enterpriseName', 'businessCode', 'operatingMonths', 'annualRevenue']
if ((result['type'] == 'enterprise' && personalOnly.includes(key)) || (result['type'] == 'personal' && enterpriseOnly.includes(key))) return
const incomeKeys = ['occupation', 'company', 'operatingMonths', 'monthlyIncome', 'annualRevenue', 'socialSecurity', 'housingFund', 'socialSecurityBase', 'housingFundBase']
const assetKeys = ['creditLoanCount', 'totalDebtBalance', 'monthlyLoanPayment', 'creditCardCount', 'creditCardTotalLimit', 'creditCardUsedLimit', 'house', 'car']
// “无”及数值0都是已填写,不能用真值判断遗漏零负债、零经营月数。
addField(incomeKeys.includes(key) ? 1 : (assetKeys.includes(key) ? 2 : 0), key, labels[index], value, raw != null && value.length > 0, raw == null)
})
return { record, fields, sections }
}
// 版本编号显式随请求传递,避免不同页面共享“当前版本”造成报告串版。
export function revisionParams(revisionId: string): UTSJSONObject { return revisionId.length > 0 ? { revisionId } : new UTSJSONObject() }
export async function fetchAssessmentRevisions(id: string, cursor: string): Promise<AssessmentPage> {
const result = await requestJson('/assessments/' + encodeURIComponent(id) + '/revisions', 'GET', { cursor })
return assessmentPageFrom(result, cursor, '', id)
}
export async function fetchAssessmentEdit(id: string): Promise<UTSJSONObject> {
const result = await requestJson('/assessments/' + encodeURIComponent(id) + '/edit', 'GET', {})
const record = recordFrom(result, id)
if (record.status == 'processing') throw new Error('评估正在处理中,请完成后再修改。')
const form = result['form'] as UTSJSONObject | null
if (form == null || Array.isArray(form) || typeof form != 'object' || result.getString('moneyUnit') != 'yuan') throw new Error('修改资料不完整,请返回档案重新读取。')
const payload: UTSJSONObject = { type: result['type'], editAssessmentId: id, baseRevisionId: result['revisionId'], step: 0 }
payload['loanCountScope'] = draftText(form, 'loanCountScope') == 'all_loans' ? 'all_loans' : ''
const money = ['monthlyIncome', 'annualRevenue', 'totalDebtBalance', 'monthlyLoanPayment', 'creditCardTotalLimit', 'creditCardUsedLimit', 'socialSecurityBase', 'housingFundBase']
const labels: UTSJSONObject = { married: '已婚', single: '未婚', divorced: '离异', widowed: '丧偶', yes: '有', no: '无', six_months_plus: '连续缴纳 6 个月以上', under_six_months: '缴纳不足 6 个月', none: '未缴纳' }
const enums = ['maritalStatus', 'house', 'car', 'socialSecurity', 'housingFund']
const keys = ['name', 'idCard', 'phone', 'city', 'occupation', 'company', 'enterpriseName', 'businessCode', 'monthlyIncome', 'annualRevenue', 'totalDebtBalance', 'monthlyLoanPayment', 'creditCardTotalLimit', 'creditCardUsedLimit', 'creditLoanCount', 'creditCardCount', 'operatingMonths', 'maritalStatus', 'house', 'car', 'socialSecurity', 'housingFund', 'socialSecurityBase', 'housingFundBase']
keys.forEach((key: string) => {
const raw = form[key]
let value = raw == null ? '' : (typeof raw == 'number' ? (raw as number).toString() : raw as string)
if (value.length > 0 && money.includes(key)) value = yuanToWan(value)
if (['socialSecurity', 'housingFund'].includes(key)) value = contributionLabel(value)
if (enums.includes(key) && labels[value] != null) value = labels[value] as string
payload[key] = value
})
return payload
}