Files
airz-uniapp/packages/finance/services/records.uts
T

348 lines
32 KiB
Plaintext
Raw Normal View History

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
}