feat(融资): 新增产品对比页面及相关接口实现
新增产品对比页面 compare/compare,支持同一评估下最多3个产品的横向对比,展示额度、期限、利率、适用条件等核心指标及匹配详情。 新增 matches.uts 服务层,封装匹配列表、详情、公共产品查询接口。 新增 account 分包的 latest-report.uts 服务,用于查找用户最新成功报告。 调整个人中心页面,将“评估记录”改为“开始评估”,新增最近报告直达功能。 更新文档,标记匹配相关接口及原型07、09页面已实现,调整页面数量至26页。 移除 finance 分包中旧的 fetchMatches、fetchProduct 模拟数据接口,统一使用正式API。
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
import { API_DEMO_ENABLED, API_BASE_URL, wait, requestJson } from '@/services/api.uts'
|
||||
import { demoProducts, parseProduct, FinanceProduct } from './finance.uts'
|
||||
import { createAssessment } from './records.uts'
|
||||
export { API_BASE_URL } from '@/services/api.uts'
|
||||
|
||||
@@ -7,26 +6,6 @@ export async function submitAssessment(payload: UTSJSONObject): Promise<string>
|
||||
return await createAssessment(payload)
|
||||
}
|
||||
|
||||
export async function fetchMatches(): Promise<FinanceProduct[]> {
|
||||
if (API_DEMO_ENABLED) {
|
||||
await wait(320)
|
||||
return demoProducts.slice()
|
||||
}
|
||||
const result = await requestJson('/matches', 'GET', new UTSJSONObject())
|
||||
const items = result['items'] as UTSJSONObject[] | null
|
||||
return items == null ? [] : items.map((item: UTSJSONObject): FinanceProduct => parseProduct(item))
|
||||
}
|
||||
|
||||
export async function fetchProduct(id: string): Promise<FinanceProduct | null> {
|
||||
if (API_DEMO_ENABLED) {
|
||||
await wait(180)
|
||||
const products = demoProducts.filter((item: FinanceProduct): boolean => item.id == id)
|
||||
return products.length > 0 ? products[0] : null
|
||||
}
|
||||
const result = await requestJson('/products/' + encodeURIComponent(id), 'GET', new UTSJSONObject())
|
||||
return parseProduct(result)
|
||||
}
|
||||
|
||||
export async function sendAssistantMessage(message: string): Promise<string> {
|
||||
if (API_DEMO_ENABLED) {
|
||||
await wait(600)
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { getSession } from '@/services/api.uts'
|
||||
import { validRecordId, MatchDetail, MatchReason } from './matches.uts'
|
||||
|
||||
let scope = ''
|
||||
let selected: string[] = []
|
||||
|
||||
export function parseCompareIds(raw: string): string[] {
|
||||
if (raw.length == 0) return []
|
||||
const ids = raw.split(',')
|
||||
if (ids.length > 3) throw new Error('每次最多对比3个产品,请返回重新选择。')
|
||||
const seen: string[] = []
|
||||
ids.forEach((id: string) => {
|
||||
if (!validRecordId(id) || seen.includes(id)) throw new Error('对比产品编号无效或重复,请返回重新选择。')
|
||||
seen.push(id)
|
||||
})
|
||||
return ids
|
||||
}
|
||||
|
||||
// 只缓存少量选择ID,不持久化产品或个人事实;换账号、会话或评估后立即隔离。
|
||||
export function compareSelection(assessmentId: string): string[] {
|
||||
const session = getSession()
|
||||
const key = session == null ? '' : (session.getString('userId') ?? '') + ':' + (session.getString('token') ?? '') + ':' + assessmentId
|
||||
if (key != scope || key.length == 0) { scope = key; selected = [] }
|
||||
return selected.slice()
|
||||
}
|
||||
export function saveCompareSelection(assessmentId: string, ids: string[]): void {
|
||||
compareSelection(assessmentId)
|
||||
selected = parseCompareIds(ids.join(','))
|
||||
}
|
||||
export function compareQuery(assessmentId: string, ids: string[]): string {
|
||||
return '?assessmentId=' + encodeURIComponent(assessmentId) + '&productIds=' + ids.join(',')
|
||||
}
|
||||
export function sameMatchSnapshot(a: MatchDetail, b: MatchDetail): boolean {
|
||||
return a.assessmentId == b.assessmentId && a.resultVersion == b.resultVersion && a.snapshotId == b.snapshotId && a.ruleVersion == b.ruleVersion && a.orderingVersion == b.orderingVersion
|
||||
}
|
||||
export function compareReasonText(reasons: MatchReason[], empty: string): string {
|
||||
return reasons.length == 0 ? empty : reasons.map((r: MatchReason): string => r.text + '\n' + r.evidence.join('\n')).join('\n\n')
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import { requestJson } from '@/services/api.uts'
|
||||
import { yuanToWan } from '@/services/money.uts'
|
||||
|
||||
export type ProductView = { id: string, version: number, name: string, bank: string, type: string, demo: boolean, amount: string, rate: string, term: string, objects: string, regions: string, valid: string, materials: string, source: string, sourceRef: string, checkedOn: string, conditions: string[] }
|
||||
export type MatchReason = { id: string, text: string, evidence: string[], fields: string[], explanation: string }
|
||||
export type MatchDetail = { assessmentId: string, resultVersion: number, snapshotId: string, ruleVersion: string, orderingVersion: string, item: MatchView }
|
||||
export type MatchView = { id: string, state: string, stateLabel: string, availability: string, availabilityLabel: string, product: ProductView, positive: MatchReason[], blocking: MatchReason[], cautions: MatchReason[], aiText: string, aiStatus: string, aiCanRetry: boolean, aiGeneratedAt: string }
|
||||
export type MatchPage = { assessmentId: string, resultVersion: number, snapshotId: string, ruleVersion: string, orderingVersion: string, status: string, message: string, eligible: string, ineligible: string, unknown: string, items: MatchView[], hasMore: boolean, cursor: string }
|
||||
|
||||
function text(data: UTSJSONObject, key: string): string { return data.getString(key) ?? '' }
|
||||
export function validRecordId(value: string): boolean { return /^[1-9][0-9]{0,9}$/.test(value) }
|
||||
export function eligibilityLabel(value: string): string { return value == 'eligible' ? '符合已知条件' : (value == 'ineligible' ? '条件不符' : '待核实') }
|
||||
const fieldNames: UTSJSONObject = { requestedAmountYuan: '融资金额', requestedTermMonths: '融资期限', applicantType: '申请主体', purposeCode: '资金用途', cityCode: '所在城市', monthlyIncome: '月收入', annualRevenue: '年营收', operatingMonths: '经营月数', totalDebtBalance: '贷款余额', monthlyLoanPayment: '每月还款额', creditLoanCount: '贷款笔数', creditCardCount: '信用卡数量', creditCardTotalLimit: '信用卡总额度', creditCardUsedLimit: '信用卡已用额度', socialSecurity: '社保情况', housingFund: '公积金情况', house: '房产情况', car: '车辆情况' }
|
||||
const labels: UTSJSONObject = { personal: '个人', enterprise: '企业', daily_consumption: '日常消费', education: '教育进修', home_improvement: '家居装修', personal_business: '个体经营周转', working_capital: '经营周转', inventory_purchase: '采购备货', equipment_purchase: '设备购置', business_expansion: '经营扩展', other: '其他用途', none: '无', under_six_months: '不足六个月', six_months_plus: '六个月及以上', yes: '有', no: '无', '440300': '深圳市', '440100': '广州市', '110100': '北京市', '310100': '上海市', '330100': '杭州市' }
|
||||
const moneyFields = ['requestedAmountYuan','monthlyIncome','annualRevenue','totalDebtBalance','monthlyLoanPayment','creditCardTotalLimit','creditCardUsedLimit']
|
||||
|
||||
function valueText(value: any | null, field: string): string {
|
||||
if (value == null) return '未提供'
|
||||
const raw = value.toString()
|
||||
if (moneyFields.includes(field)) return yuanToWan(raw) + ' 万元'
|
||||
if (field == 'requestedTermMonths' || field == 'operatingMonths') return raw + ' 个月'
|
||||
return labels.getString(raw) ?? raw
|
||||
}
|
||||
function ruleText(rule: UTSJSONObject): string {
|
||||
const field = text(rule, 'field'); const op = text(rule, 'operator')
|
||||
const prefix = fieldNames.getString(field) ?? '条件'
|
||||
let expected = ''
|
||||
if (op == 'in' || op == 'between') {
|
||||
const values = rule['value'] as any[]
|
||||
expected = values.map((value: any): string => valueText(value, field)).join(op == 'between' ? ' 至 ' : '、')
|
||||
} else expected = valueText(rule['value'], field)
|
||||
const operation = op == 'gte' ? '不少于 ' : (op == 'lte' ? '不超过 ' : (op == 'in' ? '属于 ' : ''))
|
||||
return prefix + ':' + operation + expected + (text(rule, 'requiredSource') == 'verified' ? '(需核验依据)' : '')
|
||||
}
|
||||
function productView(data: UTSJSONObject, publicMode: boolean): ProductView {
|
||||
const source = data['source'] as UTSJSONObject
|
||||
const objects = data['applicantTypes'] as string[]; const regions = data['regions'] as string[]
|
||||
const conditions: string[] = []
|
||||
if (publicMode) {
|
||||
const groups = data['rulesByApplicantType'] as UTSJSONObject
|
||||
objects.forEach((type: string) => {
|
||||
(groups[type] as UTSJSONObject[]).forEach((r: UTSJSONObject) => { conditions.push((labels.getString(type) ?? type) + ' · ' + ruleText(r)) })
|
||||
})
|
||||
} else (data['rules'] as UTSJSONObject[]).forEach((r: UTSJSONObject) => { conditions.push(ruleText(r)) })
|
||||
return { id: text(data, 'productId'), version: data.getNumber('productVersion') ?? 0, name: text(data, 'productName'), bank: text(data, 'bankName'), type: text(data, 'productType'), demo: data.getBoolean('isDemo') ?? false,
|
||||
amount: text(data, 'amountMinYuan').length == 0 || text(data, 'amountMaxYuan').length == 0 ? '未提供' : yuanToWan(text(data, 'amountMinYuan')) + '–' + yuanToWan(text(data, 'amountMaxYuan')) + ' 万元',
|
||||
rate: text(data, 'annualRateMinPercent').length == 0 || text(data, 'annualRateMaxPercent').length == 0 ? '未提供' : text(data, 'annualRateMinPercent') + '%–' + text(data, 'annualRateMaxPercent') + '%',
|
||||
term: data.getNumber('termMinMonths') == null || data.getNumber('termMaxMonths') == null ? '未提供' : data.getNumber('termMinMonths')!.toString() + '–' + data.getNumber('termMaxMonths')!.toString() + ' 个月',
|
||||
objects: objects.map((v: string): string => labels.getString(v) ?? v).join(' / '), regions: regions.length == 0 ? '全国' : regions.map((v: string): string => labels.getString(v) ?? v).join('、'),
|
||||
valid: text(data, 'validFrom') + ' 至 ' + text(data, 'validUntil'), materials: text(data, 'information').length > 0 ? text(data, 'information') : '未提供',
|
||||
source: text(source, 'publisher') + ' · ' + text(source, 'title'), sourceRef: text(source, 'reference'), checkedOn: text(source, 'checkedOn'), conditions }
|
||||
}
|
||||
function matchView(data: UTSJSONObject): MatchView {
|
||||
const evidence = data['evidence'] as UTSJSONObject[]
|
||||
const ai = data['aiExplanation'] as UTSJSONObject
|
||||
const explanations = (ai['reasonExplanations'] as UTSJSONObject[] | null) ?? []
|
||||
const reasons = (key: string): MatchReason[] => (data[key] as UTSJSONObject[]).map((r: UTSJSONObject): MatchReason => {
|
||||
const reasonFields: string[] = []
|
||||
const lines = (r['evidenceRefs'] as string[]).map((ref: string): string => {
|
||||
const e = evidence.find((item: UTSJSONObject): boolean => text(item, 'evidenceId') == ref)
|
||||
if (e == null) return '证据暂不可读取'
|
||||
const fields = e['fieldRefs'] as string[]; const field = fields.length > 0 ? fields[0] : ''
|
||||
fields.forEach((name: string) => { if (!reasonFields.includes(name)) reasonFields.push(name) })
|
||||
const source = text(e, 'source') == 'verified' ? '外部核验' : (text(e, 'source') == 'product_snapshot' ? '产品快照' : (text(e, 'source') == 'self_reported' ? '用户自填,未经核验' : '来源未知'))
|
||||
return (fieldNames.getString(field) ?? field) + ':' + valueText(e['actual'], field) + ' · ' + source
|
||||
})
|
||||
const explanation = explanations.find((item: UTSJSONObject): boolean => text(item, 'reasonId') == text(r, 'reasonId'))
|
||||
return { id: text(r, 'reasonId'), text: text(r, 'text'), evidence: lines, fields: reasonFields, explanation: text(ai, 'status') == 'completed' && explanation != null ? text(explanation, 'text') : '' }
|
||||
})
|
||||
const availability = text(data, 'currentAvailability')
|
||||
return { id: text(data, 'productId'), state: text(data, 'eligibility'), stateLabel: eligibilityLabel(text(data, 'eligibility')), availability,
|
||||
availabilityLabel: availability == 'available' ? '当前产品可查看' : (availability == 'unavailable' ? '当前产品已下架或不在有效期' : '当前产品状态暂无法确认'),
|
||||
product: productView(data['productSnapshot'] as UTSJSONObject, false), positive: reasons('positiveReasons'), blocking: reasons('blockingReasons'), cautions: reasons('cautions'),
|
||||
aiText: text(ai, 'status') == 'completed' ? text(ai, 'text') : (text(ai, 'status') == 'failed' ? 'AI解释暂不可用,已保留全部规则依据。' : '当前展示规则依据,尚未生成单产品AI解释。'),
|
||||
aiStatus: text(ai, 'status'), aiCanRetry: ai.getBoolean('canRetry') ?? false, aiGeneratedAt: text(ai, 'generatedAt') }
|
||||
}
|
||||
export async function requestProductExplanation(id: string, productId: string, requestId: string): Promise<MatchView> {
|
||||
const data = await requestJson('/assessments/' + encodeURIComponent(id) + '/matches/' + encodeURIComponent(productId) + '/explanation', 'POST', { requestId })
|
||||
const item = matchView(data['item'] as UTSJSONObject)
|
||||
if (text(data, 'assessmentId') != id || item.id != productId) throw new Error('AI解释与本次产品不一致,请重新加载。')
|
||||
return item
|
||||
}
|
||||
export async function fetchMatchPage(id: string, group: string, cursor: string): Promise<MatchPage> {
|
||||
const data = await requestJson('/assessments/' + encodeURIComponent(id) + '/matches', 'GET', { eligibility: group, limit: '3', cursor })
|
||||
const matching = data['matching'] as UTSJSONObject; const status = text(matching, 'status')
|
||||
if (text(data, 'assessmentId') != id) throw new Error('匹配结果与本次评估不一致,请重新加载。')
|
||||
const reason = text(matching, 'reasonCode')
|
||||
const count = (key: string): string => { const v = matching.getNumber(key); return v == null ? '—' : v.toString() }
|
||||
const message = status == 'completed' ? '按本次评估时的产品条件判断,展示顺序不代表优劣。' : (reason == 'LEGACY_INPUT' ? '旧版报告未执行产品匹配,可重新评估获取新的结果。' : (reason == 'MATCH_RULES_UNPUBLISHED' ? '本次评估尚未启用产品匹配规则。' : '本次评估没有可用产品目录,未执行产品匹配。'))
|
||||
return { assessmentId: id, resultVersion: data.getNumber('resultVersion') ?? 0, snapshotId: text(matching, 'catalogSnapshotId'), ruleVersion: text(matching, 'ruleVersion'), orderingVersion: text(matching, 'orderingPolicyVersion'), status, message, eligible: count('candidateCount'), ineligible: count('ineligibleCount'), unknown: count('unknownCount'), items: (data['items'] as UTSJSONObject[]).map((v: UTSJSONObject): MatchView => matchView(v)), hasMore: data.getBoolean('hasMore') ?? false, cursor: text(data, 'nextCursor') }
|
||||
}
|
||||
export async function fetchMatchDetail(id: string, productId: string): Promise<MatchView> {
|
||||
return (await fetchMatchDetailRecord(id, productId)).item
|
||||
}
|
||||
export async function fetchMatchDetailRecord(id: string, productId: string): Promise<MatchDetail> {
|
||||
const data = await requestJson('/assessments/' + encodeURIComponent(id) + '/matches/' + encodeURIComponent(productId), 'GET', new UTSJSONObject())
|
||||
const matching = data['matching'] as UTSJSONObject
|
||||
const item = matchView(data['item'] as UTSJSONObject)
|
||||
const version = data.getNumber('resultVersion') ?? 0
|
||||
const snapshotId = text(matching, 'catalogSnapshotId')
|
||||
// URL只表达选择意图;读取后再次检查归属和冻结版本,不能用公共资料补齐失败列。
|
||||
if (text(data, 'assessmentId') != id || item.id != productId || item.product.id != productId || version < 1 || snapshotId.length == 0 || text(matching, 'status') != 'completed') throw new Error('产品不属于本次冻结匹配结果,请返回重新选择。')
|
||||
return { assessmentId: id, resultVersion: version, snapshotId, ruleVersion: text(matching, 'ruleVersion'), orderingVersion: text(matching, 'orderingPolicyVersion'), item }
|
||||
}
|
||||
export async function fetchPublicProduct(id: string): Promise<ProductView> {
|
||||
return productView(await requestJson('/products/' + encodeURIComponent(id), 'GET', new UTSJSONObject()), true)
|
||||
}
|
||||
@@ -7,7 +7,22 @@ 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[] }
|
||||
export type ArchiveSection = { id: string, icon: string, title: string, note: string, fields: AssessmentField[], provided: number }
|
||||
export type AssessmentDetail = { record: AssessmentRecord, fields: AssessmentField[], sections: ArchiveSection[] }
|
||||
export type ArchiveCompleteness = { percent: number | null, description: string }
|
||||
|
||||
// 档案只消费报告中已保存的完整度,不用当前表单规则重算旧评估,也不把缺失当作0%。
|
||||
export async function fetchArchiveCompleteness(id: string): Promise<ArchiveCompleteness> {
|
||||
const report = await requestJson('/assessments/' + encodeURIComponent(id) + '/report', 'GET', {})
|
||||
if (report.getString('assessmentId') != id) throw new Error('报告与当前档案不一致')
|
||||
if (report.getNumber('resultVersion') == 1) return { percent: null, description: '这份旧版报告未计算完整度,已提交资料仍可查看。' }
|
||||
if (report.getNumber('resultVersion') != 2) throw new Error('报告格式暂不支持')
|
||||
const completeness = report['completeness'] as UTSJSONObject | null
|
||||
if (completeness == null || completeness.getString('status') != 'evaluated') return { percent: null, description: '这份报告尚未评定资料完整度。' }
|
||||
const percent = completeness.getNumber('percent')
|
||||
if (percent == null || !Number.isInteger(percent) || percent < 0 || percent > 100) throw new Error('完整度数据暂不可读取')
|
||||
return { percent, description: '反映本次评估时的资料填写情况,不代表信用评分或审批结果。' }
|
||||
}
|
||||
|
||||
function recordFrom(item: UTSJSONObject): AssessmentRecord {
|
||||
const submitted = draftText(item, 'submittedAt')
|
||||
@@ -21,11 +36,27 @@ export async function retryAssessment(id: string, requestId: string): Promise<vo
|
||||
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 type ReportMatching = { status: string, snapshotId: string, ruleVersion: string, orderingVersion: string, eligible: string, ineligible: string, unknown: string }
|
||||
export type AnalysisReport = { version: number, generatedAt: string, completenessPercent: number | null, matching: ReportMatching, 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 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) => {
|
||||
@@ -54,6 +85,8 @@ export async function fetchAnalysisReport(id: string): Promise<AnalysisReport> {
|
||||
{ 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 => {
|
||||
@@ -67,7 +100,7 @@ export async function fetchAnalysisReport(id: string): Promise<AnalysisReport> {
|
||||
}).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'),
|
||||
return { version, generatedAt, completenessPercent, matching, summary: draftText(result, 'summary'), facts, observations: explain('strengths'), suggestions: explain('improvements'), dataGaps: explain('riskWarnings'),
|
||||
// 新报告展示服务端冻结结果的统计;旧报告无此字段时保留原提示,不重算历史。
|
||||
disclaimer: draftText(result, 'disclaimer'), matchingMessage: draftText(result, 'matchingMessage').length > 0 ? draftText(result, 'matchingMessage') : '产品资料及匹配规则尚未就绪,本报告未执行产品匹配。', modelCode: draftText(result, 'modelCode'),
|
||||
demand, completenessText, completenessItems, readinessLabel: readiness['status'] == 'evaluated' ? draftText(readiness, 'label') : '未评定',
|
||||
@@ -77,7 +110,7 @@ export async function fetchAnalysisReport(id: string): Promise<AnalysisReport> {
|
||||
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: [] }
|
||||
return { version, generatedAt, completenessPercent: null, matching, 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 {
|
||||
@@ -130,8 +163,11 @@ export async function createAssessment(payload: UTSJSONObject): Promise<string>
|
||||
return id
|
||||
}
|
||||
|
||||
export async function fetchAssessments(cursor: string): Promise<AssessmentPage> {
|
||||
const result = await requestJson('/assessments', 'GET', { limit: '20', cursor })
|
||||
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)
|
||||
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') }
|
||||
}
|
||||
@@ -145,14 +181,26 @@ export async function fetchAssessmentDetail(id: string): Promise<AssessmentDetai
|
||||
const form = result['form'] as UTSJSONObject | null
|
||||
if (form == null) throw new Error('评估详情不完整')
|
||||
const fields: AssessmentField[] = []
|
||||
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, label: string, value: string, provided: boolean) => {
|
||||
const field: AssessmentField = { label, value }
|
||||
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']
|
||||
const legacy = result['inputVersion'] == null || result['inputVersion'] == 1
|
||||
// 历史记录使用提交时冻结的用途标签,不拿当前字典重新解释旧需求。
|
||||
fields.push({ label: '融资金额', value: legacy ? '当时未采集' : yuanToWan(draftText(form, 'requestedAmountYuan')) + ' 万元' } as AssessmentField)
|
||||
addField(0, '融资金额', legacy ? '当时未采集' : yuanToWan(draftText(form, 'requestedAmountYuan')) + ' 万元', !legacy && form['requestedAmountYuan'] != null)
|
||||
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)
|
||||
addField(0, '融资期限', legacy || term == null ? '当时未采集' : term.toString() + ' 个月', !legacy && term != null)
|
||||
addField(0, '资金用途', legacy ? '当时未采集' : draftText(form, 'purposeLabel'), !legacy && form['purposeCode'] != null)
|
||||
if (form['purposeCode'] == 'other') addField(0, '用途说明', draftText(form, 'purposeDetail'), draftText(form, 'purposeDetail').length > 0)
|
||||
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: '未缴纳' }
|
||||
@@ -165,7 +213,10 @@ export async function fetchAssessmentDetail(id: string): Promise<AssessmentDetai
|
||||
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)
|
||||
const incomeKeys = ['occupation', 'company', 'operatingMonths', 'monthlyIncome', 'annualRevenue', 'socialSecurity', 'housingFund']
|
||||
const assetKeys = ['creditLoanCount', 'totalDebtBalance', 'monthlyLoanPayment', 'creditCardCount', 'creditCardTotalLimit', 'creditCardUsedLimit', 'house', 'car']
|
||||
// “无”及数值0都是已填写,不能用真值判断遗漏零负债、零经营月数。
|
||||
addField(incomeKeys.includes(key) ? 1 : (assetKeys.includes(key) ? 2 : 0), labels[index], value, raw != null && value.length > 0)
|
||||
})
|
||||
return { record: recordFrom(result), fields }
|
||||
return { record: recordFrom(result), fields, sections }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user