171 lines
15 KiB
Plaintext
171 lines
15 KiB
Plaintext
import { getSession, requestJson } from '@/services/api.uts'
|
||||
|
|
import { assessmentApiPayload, draftText } from '@/services/assessment.uts'
|
|||
|
|
import { assessmentRequestKey } from '@/services/assessment-flow.uts'
|
|||
|
|
import { yuanToWan, wanToYuan } from '@/services/money.uts'
|
|||
|
|
import { demandValidationMessage } from '@/services/assessment-demand.uts'
|
|||
|
|
|
|||
|
|
export type AssessmentRecord = { id: string, type: string, status: string, statusLabel: string, message: string, city: string, submittedAt: string, taskId: string, stage: string, canRetry: boolean }
|
|||
|
|
export type AssessmentPage = { items: AssessmentRecord[], hasMore: boolean, nextCursor: string }
|
|||
|
|
export type AssessmentField = { label: string, value: string }
|
|||
|
|
export type AssessmentDetail = { record: AssessmentRecord, fields: AssessmentField[] }
|
|||
|
|
|
|||
|
|
function recordFrom(item: UTSJSONObject): AssessmentRecord {
|
|||
|
|
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 }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export async function retryAssessment(id: string, requestId: string): Promise<void> {
|
|||
|
|
await requestJson('/assessments/' + encodeURIComponent(id) + '/retry', 'POST', { requestId })
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export type AnalysisReport = { version: number, summary: string, facts: AssessmentField[], observations: string[], suggestions: string[], dataGaps: string[], disclaimer: string, matchingMessage: string, modelCode: string, demand: AssessmentField[], completenessText: string, completenessItems: AssessmentField[], readinessLabel: string, readinessBasis: string[], warnings: string[] }
|
|||
|
|
export async function fetchAnalysisReport(id: string): Promise<AnalysisReport> {
|
|||
|
|
const result = await requestJson('/assessments/' + encodeURIComponent(id) + '/report', 'GET', {})
|
|||
|
|
const version = result['resultVersion'] as number
|
|||
|
|
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 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['state'] == 'invalid') value = '格式需核实'
|
|||
|
|
const enumLabels: UTSJSONObject = { six_months_plus: '连续缴纳6个月以上', under_six_months: '缴纳不足6个月', none: '无' }
|
|||
|
|
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[] = [
|
|||
|
|
{ label: '融资金额', value: yuanToWan(draftText(demandData, 'requestedAmountYuan')) + ' 万元' },
|
|||
|
|
{ label: '融资期限', value: (demandData['requestedTermMonths'] as number).toString() + ' 个月' },
|
|||
|
|
{ label: '资金用途', value: draftText(demandData, 'purposeLabel') }
|
|||
|
|
]
|
|||
|
|
const evaluated = completeness['status'] == 'evaluated'
|
|||
|
|
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 { version, summary: draftText(result, 'summary'), facts, observations: explain('strengths'), suggestions: explain('improvements'), dataGaps: explain('riskWarnings'),
|
|||
|
|
disclaimer: draftText(result, 'disclaimer'), matchingMessage: '产品资料及匹配规则尚未就绪,本报告未执行产品匹配。', modelCode: draftText(result, 'modelCode'),
|
|||
|
|
demand, completenessText, completenessItems, 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')
|
|||
|
|
})
|
|||
|
|
return { version, summary: draftText(result, 'summary'), facts, observations, suggestions: result['suggestions'] as string[], dataGaps: result['dataGaps'] as string[], disclaimer: draftText(result, 'disclaimer'), matchingMessage: draftText(result, 'matchingMessage'), modelCode: draftText(result, 'modelCode'), demand: [], completenessText: '旧版报告未计算资料完整度', completenessItems: [], readinessLabel: '未评定', readinessBasis: [], warnings: [] }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
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 demandError = demandValidationMessage(draftText(payload, 'requestedAmountWan'), draftText(payload, 'requestedTermMonths'), draftText(payload, 'purposeCode'), draftText(payload, 'purposeDetail'))
|
|||
|
|
if (demandError.length > 0) throw new Error(demandError)
|
|||
|
|
const documents = payload['documents'] as UTSJSONObject[] | null
|
|||
|
|
const grants = payload['authorizations'] as string[] | null
|
|||
|
|
if ((documents != null && documents.length > 0) || (grants != null && grants.length > 0)) 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']
|
|||
|
|
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)
|
|||
|
|
})
|
|||
|
|
form['maritalStatus'] = enumValue(draftText(values, 'maritalStatus'), ['已婚', '未婚', '离异', '丧偶'], ['married', 'single', 'divorced', 'widowed'])
|
|||
|
|
const socialKeys = ['socialSecurity', 'housingFund']
|
|||
|
|
socialKeys.forEach((key: string) => { form[key] = enumValue(draftText(values, key), ['连续缴纳 6 个月以上', '缴纳不足 6 个月', '未缴纳'], ['six_months_plus', 'under_six_months', 'none']) })
|
|||
|
|
const ownershipKeys = ['house', 'car']
|
|||
|
|
ownershipKeys.forEach((key: string) => { form[key] = enumValue(draftText(values, key), ['有', '无'], ['yes', 'no']) })
|
|||
|
|
const cityNames = ['深圳市', '广州市', '北京市', '上海市', '杭州市']
|
|||
|
|
const cityCodes = ['440300', '440100', '110100', '310100', '330100']
|
|||
|
|
const cityIndex = cityNames.indexOf(draftText(values, 'city'))
|
|||
|
|
form['cityCode'] = cityIndex < 0 ? null : cityCodes[cityIndex]
|
|||
|
|
// 需求金额单独命名单位,仅在提交边界换算,避免与收入及负债混用。
|
|||
|
|
form['requestedAmountYuan'] = wanToYuan(draftText(payload, 'requestedAmountWan'))
|
|||
|
|
form['requestedTermMonths'] = parseInt(draftText(payload, 'requestedTermMonths'))
|
|||
|
|
form['purposeCode'] = draftText(payload, 'purposeCode')
|
|||
|
|
form['purposeDetail'] = form['purposeCode'] == 'other' ? draftText(payload, 'purposeDetail').trim() : null
|
|||
|
|
const body: UTSJSONObject = { inputVersion: 2, type: values['type'], moneyUnit: 'yuan', form, documents: [] as UTSJSONObject[], consentIds: [] as string[] }
|
|||
|
|
body['requestId'] = assessmentRequestKey(session['token'] as string, JSON.stringify(body))
|
|||
|
|
const result = await requestJson('/assessments', 'POST', body)
|
|||
|
|
const id = draftText(result, 'assessmentId')
|
|||
|
|
if (id.length == 0) throw new Error('提交回执不完整,请重试')
|
|||
|
|
return id
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export async function fetchAssessments(cursor: string): Promise<AssessmentPage> {
|
|||
|
|
const result = await requestJson('/assessments', 'GET', { limit: '20', cursor })
|
|||
|
|
const items = result['items'] as UTSJSONObject[] | null
|
|||
|
|
return { items: items == null ? [] : items.map((item: UTSJSONObject): AssessmentRecord => recordFrom(item)), hasMore: result['hasMore'] == true, nextCursor: draftText(result, 'nextCursor') }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export async function fetchAssessmentStatus(id: string): Promise<AssessmentRecord> {
|
|||
|
|
return recordFrom(await requestJson('/assessments/' + encodeURIComponent(id) + '/status', 'GET', {}))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export async function fetchAssessmentDetail(id: string): Promise<AssessmentDetail> {
|
|||
|
|
const result = await requestJson('/assessments/' + encodeURIComponent(id), 'GET', {})
|
|||
|
|
const form = result['form'] as UTSJSONObject | null
|
|||
|
|
if (form == null) throw new Error('评估详情不完整')
|
|||
|
|
const fields: AssessmentField[] = []
|
|||
|
|
const keys = ['name', 'phone', 'city', 'idCard', 'enterpriseName', 'businessCode', 'occupation', 'company', 'operatingMonths', 'monthlyIncome', 'annualRevenue', 'creditLoanCount', 'totalDebtBalance', 'monthlyLoanPayment', 'creditCardCount', 'creditCardTotalLimit', 'creditCardUsedLimit', 'maritalStatus', 'socialSecurity', 'housingFund', 'house', 'car']
|
|||
|
|
const legacy = result['inputVersion'] == null || result['inputVersion'] == 1
|
|||
|
|
// 历史记录使用提交时冻结的用途标签,不拿当前字典重新解释旧需求。
|
|||
|
|
fields.push({ label: '融资金额', value: legacy ? '当时未采集' : yuanToWan(draftText(form, 'requestedAmountYuan')) + ' 万元' } as AssessmentField)
|
|||
|
|
const term = form['requestedTermMonths'] as number | null
|
|||
|
|
fields.push({ label: '融资期限', value: legacy || term == null ? '当时未采集' : term.toString() + ' 个月' } as AssessmentField)
|
|||
|
|
fields.push({ label: '资金用途', value: legacy ? '当时未采集' : draftText(form, 'purposeLabel') } as AssessmentField)
|
|||
|
|
if (form['purposeCode'] == 'other') fields.push({ label: '用途说明', value: draftText(form, 'purposeDetail') } as AssessmentField)
|
|||
|
|
const labels = ['姓名 / 联系人', '手机号', '城市', '身份证号', '企业名称', '信用代码', '职业 / 行业', '工作单位', '经营月数', '月收入', '年营收', '信用贷款笔数', '欠款总余额', '贷款月还款金额', '信用卡张数', '信用卡总额度', '信用卡已用额度', '婚姻状况', '社保缴纳', '公积金缴纳', '是否有房', '是否有车']
|
|||
|
|
const moneyKeys = ['monthlyIncome', 'annualRevenue', 'totalDebtBalance', 'monthlyLoanPayment', 'creditCardTotalLimit', 'creditCardUsedLimit']
|
|||
|
|
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 (['maritalStatus', 'socialSecurity', 'housingFund', 'house', 'car'].includes(key) && enumLabels[value] != null) value = enumLabels[value] as string
|
|||
|
|
const personalOnly = ['idCard', 'company', 'monthlyIncome', 'maritalStatus', 'socialSecurity', 'housingFund']
|
|||
|
|
const enterpriseOnly = ['enterpriseName', 'businessCode', 'operatingMonths', 'annualRevenue']
|
|||
|
|
if ((result['type'] == 'enterprise' && personalOnly.includes(key)) || (result['type'] == 'personal' && enterpriseOnly.includes(key))) return
|
|||
|
|
const field: AssessmentField = { label: labels[index], value }; fields.push(field)
|
|||
|
|
})
|
|||
|
|
return { record: recordFrom(result), fields }
|
|||
|
|
}
|