feat: 支持评估版本隔离的报告、匹配及PDF导出功能

新增revisionId参数到路由、服务及导出接口,实现不同评估版本的隔离访问。删除已废弃的需求相关服务文件,移除取消采集的融资金额、期限和用途字段,更新相关页面提示文本。同时更新文档,说明评估版本、最新资料及产品展示规则的调整,支持前台正常业务名称显示,去除环境标签。
This commit is contained in:
hujun
2026-09-30 19:11:44 +08:00
parent 542c107317
commit 3b2a0139e4
34 changed files with 1389 additions and 338 deletions
+10 -9
View File
@@ -1,6 +1,7 @@
import { revisionParams } from './records.uts'
import { requestJson } from '@/services/api.uts'
export type AssistantProduct = { id: string, name: string, state: string, stateLabel: string, demo: boolean }
export type AssistantContext = { assessmentId: string, productIds: string[], title: string, products: AssistantProduct[], questions: string[], notice: string }
export type AssistantContext = { revisionId: string, assessmentId: string, productIds: string[], title: string, products: AssistantProduct[], questions: string[], notice: string }
export type AssistantChat = { id: string, title: string, context: AssistantContext }
export type AssistantTurn = { id: string, requestId: string, message: string, reply: string, status: string, attempt: number, canRetry: boolean }
export type AssistantPage = { items: AssistantTurn[], more: boolean, cursor: string }
@@ -8,24 +9,24 @@ export type AssistantHistory = { id: string, title: string, createdAt: string }
export type AssistantHistoryPage = { items: AssistantHistory[], more: boolean, cursor: string }
function text(data: UTSJSONObject, key: string): string { return data.getString(key) ?? '' }
function contextView(data: UTSJSONObject): AssistantContext {
return { assessmentId: text(data, 'assessmentId'), productIds: data['productIds'] as string[], title: text(data, 'title'),
return { revisionId: text(data, 'revisionId'), assessmentId: text(data, 'assessmentId'), productIds: data['productIds'] as string[], title: text(data, 'title'),
products: (data['products'] as UTSJSONObject[]).map((p: UTSJSONObject): AssistantProduct => ({ id: text(p, 'id'), name: text(p, 'name'), state: text(p, 'state'), stateLabel: text(p, 'stateLabel'), demo: p.getBoolean('isDemo') ?? false })),
questions: data['questions'] as string[], notice: text(data, 'notice') }
}
function chatView(data: UTSJSONObject): AssistantChat { return { id: text(data, 'chatId'), title: text(data, 'title'), context: contextView(data['context'] as UTSJSONObject) } }
function turnView(data: UTSJSONObject): AssistantTurn { return { id: text(data, 'id'), requestId: text(data, 'requestId'), message: text(data, 'message'), reply: text(data, 'reply'), status: text(data, 'status'), attempt: data.getNumber('attempt') ?? 0, canRetry: data.getBoolean('canRetry') ?? false } }
function contextQuery(assessmentId: string, ids: string[]): UTSJSONObject { return { contextAssessmentId: assessmentId, contextProductIds: ids.join(',') } }
function contextQuery(assessmentId: string, ids: string[], revisionId: string): UTSJSONObject { return { ...revisionParams(revisionId), contextAssessmentId: assessmentId, contextProductIds: ids.join(',') } }
export function assistantKey(): string { return 'chat_' + Date.now().toString() + '_' + Math.floor(Math.random() * 1000000000).toString() }
export function assistantRoute(assessmentId: string, ids: string[], from: string, chatId: string = ''): string {
return '/packages/finance/assistant/assistant?from=' + from + (assessmentId.length > 0 ? '&assessmentId=' + assessmentId + '&productIds=' + ids.join(',') : '') + (chatId.length > 0 ? '&chatId=' + chatId : '')
export function assistantRoute(assessmentId: string, ids: string[], from: string, chatId: string = '', revisionId: string = ''): string {
return '/packages/finance/assistant/assistant?from=' + from + (assessmentId.length > 0 ? '&assessmentId=' + assessmentId + '&productIds=' + ids.join(',') : '') + (chatId.length > 0 ? '&chatId=' + chatId : '') + (revisionId.length > 0 ? '&revisionId=' + revisionId : '')
}
export async function previewAssistant(assessmentId: string, ids: string[]): Promise<AssistantContext> { return contextView(await requestJson('/assistant/context', 'GET', contextQuery(assessmentId, ids))) }
export async function loadAssistant(chatId: string, assessmentId: string, ids: string[], checkBinding: boolean): Promise<AssistantChat> {
const result = chatView(await requestJson('/assistant/chats/' + chatId, 'GET', checkBinding ? contextQuery(assessmentId, ids) : new UTSJSONObject()))
export async function previewAssistant(assessmentId: string, ids: string[], revisionId: string = ''): Promise<AssistantContext> { return contextView(await requestJson('/assistant/context', 'GET', contextQuery(assessmentId, ids, revisionId))) }
export async function loadAssistant(chatId: string, assessmentId: string, ids: string[], checkBinding: boolean, revisionId: string = ''): Promise<AssistantChat> {
const result = chatView(await requestJson('/assistant/chats/' + chatId, 'GET', checkBinding ? contextQuery(assessmentId, ids, revisionId) : new UTSJSONObject()))
if (result.id != chatId) throw new Error('返回的会话不一致,请重新加载。')
return result
}
export async function createAssistant(assessmentId: string, ids: string[], requestId: string): Promise<AssistantChat> { return chatView(await requestJson('/assistant/chats', 'POST', { contextAssessmentId: assessmentId.length > 0 ? assessmentId : null, contextProductIds: ids, requestId })) }
export async function createAssistant(assessmentId: string, ids: string[], requestId: string, revisionId: string = ''): Promise<AssistantChat> { return chatView(await requestJson('/assistant/chats', 'POST', { ...revisionParams(revisionId), contextAssessmentId: assessmentId.length > 0 ? assessmentId : null, contextProductIds: ids, requestId })) }
export async function assistantMessages(chatId: string, cursor: string): Promise<AssistantPage> {
const data = await requestJson('/assistant/chats/' + chatId + '/messages', 'GET', { limit: '20', cursor })
if (text(data, 'chatId') != chatId) throw new Error('消息不属于本次会话。')
+7 -7
View File
@@ -17,21 +17,21 @@ export function parseCompareIds(raw: string): string[] {
}
// 只缓存少量选择ID,不持久化产品或个人事实;换账号、会话或评估后立即隔离。
export function compareSelection(assessmentId: string): string[] {
export function compareSelection(assessmentId: string, revisionId: string = ''): string[] {
const session = getSession()
const key = session == null ? '' : (session.getString('userId') ?? '') + ':' + (session.getString('token') ?? '') + ':' + assessmentId
const key = session == null ? '' : (session.getString('userId') ?? '') + ':' + (session.getString('token') ?? '') + ':' + assessmentId + ':' + revisionId
if (key != scope || key.length == 0) { scope = key; selected = [] }
return selected.slice()
}
export function saveCompareSelection(assessmentId: string, ids: string[]): void {
compareSelection(assessmentId)
export function saveCompareSelection(assessmentId: string, ids: string[], revisionId: string = ''): void {
compareSelection(assessmentId, revisionId)
selected = parseCompareIds(ids.join(','))
}
export function compareQuery(assessmentId: string, ids: string[]): string {
return '?assessmentId=' + encodeURIComponent(assessmentId) + '&productIds=' + ids.join(',')
export function compareQuery(assessmentId: string, ids: string[], revisionId: string = ''): string {
return '?assessmentId=' + encodeURIComponent(assessmentId) + '&productIds=' + ids.join(',') + (revisionId.length > 0 ? '&revisionId=' + revisionId : '')
}
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
return a.assessmentId == b.assessmentId && a.revisionId == b.revisionId && 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')
+20 -18
View File
@@ -1,11 +1,12 @@
import { revisionParams } from './records.uts'
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 }
export type MatchDetail = { assessmentId: string, revisionId: 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, aiCanRequest: boolean, aiGeneratedAt: string }
export type MatchPage = { assessmentId: string, revisionId: 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) }
@@ -48,7 +49,8 @@ function productView(data: UTSJSONObject, publicMode: boolean): ProductView {
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 }
// 内部引用用于定位资料来源,不把测试环境标识当作面向用户的产品说明。
source: text(source, 'publisher') + ' · ' + text(source, 'title'), sourceRef: text(source, 'reference').startsWith('demo:airz-') ? '产品规则档案' : text(source, 'reference'), checkedOn: text(source, 'checkedOn'), conditions }
}
function matchView(data: UTSJSONObject): MatchView {
const evidence = data['evidence'] as UTSJSONObject[]
@@ -72,35 +74,35 @@ function matchView(data: UTSJSONObject): MatchView {
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') }
aiStatus: text(ai, 'status'), aiCanRetry: ai.getBoolean('canRetry') ?? false, aiCanRequest: data.getBoolean('canRequestExplanation') ?? 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 })
export async function requestProductExplanation(id: string, productId: string, requestId: string, revisionId: string = ''): Promise<MatchView> {
const data = await requestJson('/assessments/' + encodeURIComponent(id) + '/matches/' + encodeURIComponent(productId) + '/explanation', 'POST', { ...revisionParams(revisionId), requestId })
const item = matchView(data['item'] as UTSJSONObject)
if (text(data, 'assessmentId') != id || item.id != productId) throw new Error('AI解释与本次产品不一致,请重新加载。')
if ((revisionId.length > 0 && text(data, 'revisionId') != revisionId) || 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 })
export async function fetchMatchPage(id: string, group: string, cursor: string, revisionId: string = ''): Promise<MatchPage> {
const data = await requestJson('/assessments/' + encodeURIComponent(id) + '/matches', 'GET', { ...revisionParams(revisionId), eligibility: group, limit: '3', cursor })
const matching = data['matching'] as UTSJSONObject; const status = text(matching, 'status')
if (text(data, 'assessmentId') != id) throw new Error('匹配结果与本次评估不一致,请重新加载。')
if ((revisionId.length > 0 && text(data, 'revisionId') != revisionId) || 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') }
return { assessmentId: id, revisionId: text(data, 'revisionId'), 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 fetchMatchDetail(id: string, productId: string, revisionId: string = ''): Promise<MatchView> {
return (await fetchMatchDetailRecord(id, productId, revisionId)).item
}
export async function fetchMatchDetailRecord(id: string, productId: string): Promise<MatchDetail> {
const data = await requestJson('/assessments/' + encodeURIComponent(id) + '/matches/' + encodeURIComponent(productId), 'GET', new UTSJSONObject())
export async function fetchMatchDetailRecord(id: string, productId: string, revisionId: string = ''): Promise<MatchDetail> {
const data = await requestJson('/assessments/' + encodeURIComponent(id) + '/matches/' + encodeURIComponent(productId), 'GET', revisionParams(revisionId))
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 }
if ((revisionId.length > 0 && text(data, 'revisionId') != revisionId) || 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, revisionId: text(data, 'revisionId'), 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)
+48 -32
View File
@@ -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
}
+2 -2
View File
@@ -3,7 +3,7 @@ import { APP_ENVIRONMENT } from '@/services/config.uts'
export type ReportDownload = { cancel: () => void, done: Promise<string> }
export function downloadReportPdf(id: string): ReportDownload {
export function downloadReportPdf(id: string, revisionId: string = ''): ReportDownload {
// 首版仅开放普通浏览器H5;其他端明确提示,不生成假文件或成功回执。
// #ifndef H5
return { cancel: () => {}, done: new Promise<string>((_resolve, reject) => { reject(new Error('当前客户端暂未开放PDF下载,请在普通浏览器打开H5并登录后下载。')) }) }
@@ -23,7 +23,7 @@ export function downloadReportPdf(id: string): ReportDownload {
}
const timeout = setTimeout(() => controller.abort(), 45000)
try {
const response = await fetch(API_BASE_URL + '/assessments/' + id + '/report.pdf', {
const response = await fetch(API_BASE_URL + '/assessments/' + id + '/report.pdf' + (revisionId.length > 0 ? '?revisionId=' + encodeURIComponent(revisionId) : ''), {
method: 'GET', headers: { Authorization: 'Bearer ' + token, 'X-Requested-With': 'XMLHttpRequest' },
cache: 'no-store', credentials: 'omit', redirect: 'error', signal: controller.signal
})