feat: 支持评估版本隔离的报告、匹配及PDF导出功能
新增revisionId参数到路由、服务及导出接口,实现不同评估版本的隔离访问。删除已废弃的需求相关服务文件,移除取消采集的融资金额、期限和用途字段,更新相关页面提示文本。同时更新文档,说明评估版本、最新资料及产品展示规则的调整,支持前台正常业务名称显示,去除环境标签。
This commit is contained in:
@@ -1,10 +1,9 @@
|
||||
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'
|
||||
import { yuanToWan } from '@/services/money.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 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 ArchiveSection = { id: string, icon: string, title: string, note: string, fields: AssessmentField[], provided: number }
|
||||
@@ -12,8 +11,8 @@ export type AssessmentDetail = { record: AssessmentRecord, fields: AssessmentFie
|
||||
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', {})
|
||||
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 (report.getNumber('resultVersion') == 1) return { percent: null, description: '这份旧版报告未计算完整度,已提交资料仍可查看。' }
|
||||
if (report.getNumber('resultVersion') != 2) throw new Error('报告格式暂不支持')
|
||||
@@ -29,7 +28,7 @@ function recordFrom(item: UTSJSONObject): AssessmentRecord {
|
||||
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 }
|
||||
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> {
|
||||
@@ -37,10 +36,11 @@ export async function retryAssessment(id: string, requestId: string): Promise<vo
|
||||
}
|
||||
|
||||
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', {})
|
||||
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[], suggestionSummaries: 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, revisionId: string = ''): Promise<AnalysisReport> {
|
||||
const result = await requestJson('/assessments/' + encodeURIComponent(id) + '/report', 'GET', revisionParams(revisionId))
|
||||
const version = result['resultVersion'] as number
|
||||
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()
|
||||
@@ -79,7 +79,7 @@ export async function fetchAnalysisReport(id: string): Promise<AnalysisReport> {
|
||||
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[] = [
|
||||
const demand: AssessmentField[] = result['inputVersion'] == 3 ? [] : [
|
||||
{ label: '融资金额', value: yuanToWan(draftText(demandData, 'requestedAmountYuan')) + ' 万元' },
|
||||
{ label: '融资期限', value: (demandData['requestedTermMonths'] as number).toString() + ' 个月' },
|
||||
{ label: '资金用途', value: draftText(demandData, 'purposeLabel') }
|
||||
@@ -100,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, generatedAt, completenessPercent, matching, summary: draftText(result, 'summary'), facts, observations: explain('strengths'), suggestions: explain('improvements'), dataGaps: explain('riskWarnings'),
|
||||
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'), suggestionSummaries: (result['improvements'] as UTSJSONObject[]).map((item: UTSJSONObject): string => draftText(item, 'text')), 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') : '未评定',
|
||||
@@ -110,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, 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: [] }
|
||||
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[], suggestionSummaries: 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 {
|
||||
@@ -123,8 +123,6 @@ function enumValue(value: string, labels: string[], codes: string[]): string | n
|
||||
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('资料上传与外部查询授权尚未开放,请移除资料及授权选择后提交')
|
||||
@@ -150,14 +148,14 @@ export async function createAssessment(payload: UTSJSONObject): Promise<string>
|
||||
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)
|
||||
// 输入v3恢复原资料表单,不提交已取消的资金需求字段;报告仍使用v2展示结构。
|
||||
const body: UTSJSONObject = { inputVersion: 3, type: values['type'], moneyUnit: 'yuan', form, documents: [] as UTSJSONObject[], consentIds: [] as string[] }
|
||||
const editId = draftText(payload, 'editAssessmentId')
|
||||
if (editId.length > 0) body['baseRevisionId'] = draftText(payload, 'baseRevisionId')
|
||||
// 新建和修改使用不同幂等作用域,回执丢失时安全重放原提交。
|
||||
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')
|
||||
if (id.length == 0) throw new Error('提交回执不完整,请重试')
|
||||
return id
|
||||
@@ -176,14 +174,14 @@ export async function fetchAssessmentStatus(id: string): Promise<AssessmentRecor
|
||||
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', {})
|
||||
export async function fetchAssessmentDetail(id: string, revisionId: string = ''): Promise<AssessmentDetail> {
|
||||
const result = await requestJson('/assessments/' + encodeURIComponent(id), 'GET', revisionParams(revisionId))
|
||||
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: '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 }
|
||||
@@ -194,13 +192,6 @@ export async function fetchAssessmentDetail(id: string): Promise<AssessmentDetai
|
||||
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
|
||||
// 历史记录使用提交时冻结的用途标签,不拿当前字典重新解释旧需求。
|
||||
addField(0, '融资金额', legacy ? '当时未采集' : yuanToWan(draftText(form, 'requestedAmountYuan')) + ' 万元', !legacy && form['requestedAmountYuan'] != null)
|
||||
const term = form['requestedTermMonths'] as number | null
|
||||
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: '未缴纳' }
|
||||
@@ -220,3 +211,28 @@ export async function fetchAssessmentDetail(id: string): Promise<AssessmentDetai
|
||||
})
|
||||
return { record: recordFrom(result), 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 { items: (result['items'] as UTSJSONObject[]).map((item: UTSJSONObject): AssessmentRecord => recordFrom(item)), hasMore: result.getBoolean('hasMore') ?? false, nextCursor: draftText(result, 'nextCursor') }
|
||||
}
|
||||
export async function fetchAssessmentEdit(id: string): Promise<UTSJSONObject> {
|
||||
const result = await requestJson('/assessments/' + encodeURIComponent(id) + '/edit', 'GET', {})
|
||||
if (draftText(result, 'assessmentId') != id || result.getString('status') == 'processing') throw new Error('评估正在处理中,请完成后再修改。')
|
||||
const form = result['form'] as UTSJSONObject
|
||||
const payload: UTSJSONObject = { type: result['type'], editAssessmentId: id, baseRevisionId: result['revisionId'], step: 0 }
|
||||
const money = ['monthlyIncome', 'annualRevenue', 'totalDebtBalance', 'monthlyLoanPayment', 'creditCardTotalLimit', 'creditCardUsedLimit']
|
||||
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']
|
||||
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 (enums.includes(key) && labels[value] != null) value = labels[value] as string
|
||||
payload[key] = value
|
||||
})
|
||||
return payload
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user