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 { 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 { 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 { return (await fetchMatchDetailRecord(id, productId)).item } export async function fetchMatchDetailRecord(id: string, productId: string): Promise { 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 { return productView(await requestJson('/products/' + encodeURIComponent(id), 'GET', new UTSJSONObject()), true) }