feat: 重构评估流程为内存状态管理并接入真实提交与报告接口

本次重构将评估草稿改为内存状态管理,移除本地存储草稿,新增评估流程内存状态模块;接入真实的账户资料获取、头像上传接口,新增账户缓存清理逻辑;重构评估提交逻辑,接入真实提交接口并返回评估ID;新增分析页和报告页的真实状态查询、报告获取功能;更新README文档,记录开发进度和接口状态。
This commit is contained in:
hujun
2026-09-24 11:50:07 +08:00
parent dcf12a4471
commit dbfd637388
22 changed files with 1468 additions and 434 deletions
+43 -3
View File
@@ -1,5 +1,7 @@
/** Shared request, configuration and session infrastructure. */
import { API_BASE_URL, APP_ENVIRONMENT } from './config.uts'
import { clearAccountCache } from './cache-lifecycle.uts'
import { clearAssessmentFlow } from './assessment-flow.uts'
export { API_BASE_URL, API_DEMO_ENABLED } from './config.uts'
const REQUEST_TIMEOUT = 10000
const SESSION_KEY = 'aiFinanceSession'
@@ -10,6 +12,8 @@ export function saveSession(token: string, phone: string, userId: string, expire
if (token.length == 0 || userId.length == 0 || expiresAt.length == 0) throw new Error('登录响应不完整')
const expiry = Date.parse(expiresAt)
if (isNaN(expiry) || expiry <= Date.now()) throw new Error('登录凭证已过期')
const previous = getSession()
if (previous != null && previous['userId'] != userId) clearAccountCache(previous['userId'] as string)
uni.setStorageSync(SESSION_KEY, { token, phone, userId, expiresAt, loggedInAt: Date.now() })
}
@@ -21,16 +25,24 @@ export function getSession(): UTSJSONObject | null {
const userId = session['userId'] as string | null
const expiresAt = session['expiresAt'] as string | null
if (token == null || userId == null || expiresAt == null || token.length == 0 || userId.length == 0 || token.startsWith('demo-')) {
uni.removeStorageSync(SESSION_KEY)
clearSession()
return null
}
const expiry = Date.parse(expiresAt)
if (isNaN(expiry) || expiry <= Date.now()) { uni.removeStorageSync(SESSION_KEY); return null }
if (isNaN(expiry) || expiry <= Date.now()) { clearSession(); return null }
return session
}
export function isLoggedIn(): boolean { return getSession() != null }
export function clearSession(): void { uni.removeStorageSync(SESSION_KEY) }
export function clearSession(): void {
clearAssessmentFlow()
const value = uni.getStorageSync(SESSION_KEY)
if (value != null && typeof value == 'object') {
const userId = (value as UTSJSONObject)['userId'] as string | null
if (userId != null) clearAccountCache(userId)
}
uni.removeStorageSync(SESSION_KEY)
}
const currentToken = (): string => { const session = getSession(); return session == null ? '' : session['token'] as string }
const apiMessage = (error: any | null, fallback: string): string => {
@@ -75,3 +87,31 @@ const requestJson = (path: string, method: RequestMethod, payload: UTSJSONObject
}
export { apiMessage, requestJson, wait }
export function uploadJson(path: string, filePath: string): Promise<UTSJSONObject> {
return new Promise<UTSJSONObject>((resolve, reject) => {
const sentToken = currentToken()
if (sentToken.length == 0) { reject(new Error('请先登录')); return }
if (API_BASE_URL.length == 0 || (APP_ENVIRONMENT == 'production' && !API_BASE_URL.startsWith('https://'))) {
reject(new Error('服务地址尚未配置')); return
}
uni.uploadFile({ url: API_BASE_URL + path, filePath, name: 'file', timeout: REQUEST_TIMEOUT,
header: { Authorization: 'Bearer ' + sentToken, 'X-Requested-With': 'XMLHttpRequest' },
success: (response) => {
if (currentToken() != sentToken) { reject(new Error('登录状态已变化,请重新操作')); return }
if (response.statusCode == 401) clearSession()
try {
const body = JSON.parse<UTSJSONObject>(response.data)
if (body == null) throw new Error('上传响应格式错误')
if (response.statusCode < 200 || response.statusCode >= 300 || body['code'] != 1) {
if (body['code'] == 401) clearSession()
throw new Error((body['msg'] as string | null) ?? '头像上传失败')
}
const data = body['data'] as UTSJSONObject | null
if (data == null) throw new Error('上传响应不完整')
resolve(data)
} catch (error) { reject(error) }
}, fail: () => { reject(new Error('头像上传失败,请检查网络后重试')) }
})
})
}
+31
View File
@@ -0,0 +1,31 @@
import { requestJson } from './api.uts'
import { wanAmountValid, wanToCents } from './money.uts'
export type FinancingPurpose = { type: string, code: string, label: string, requiresDetail: boolean }
// 用途由服务端发布;加载失败时提示重试,不用本机猜测字典后提交。
export async function fetchFinancingPurposes(): Promise<FinancingPurpose[]> {
const result = await requestJson('/dictionaries', 'GET', {})
const versions = result['assessmentInputVersions'] as number[] | null
const minimum = result['minNewAssessmentInputVersion'] as number | null
if (versions == null || !versions.includes(2) || minimum == null || minimum > 2) throw new Error('评估版本已更新,请刷新页面后重试')
const dictionary = result['financingPurposes'] as UTSJSONObject | null
if (dictionary == null) throw new Error('资金用途暂不可用,请重试')
const items = dictionary['items'] as UTSJSONObject[] | null
if (items == null) throw new Error('资金用途暂不可用,请重试')
const purposes: FinancingPurpose[] = []
items.forEach((item: UTSJSONObject) => {
if (item['enabled'] == true) purposes.push({ type: item['type'] as string, code: item['code'] as string, label: item['label'] as string, requiresDetail: item['requiresDetail'] == true } as FinancingPurpose)
})
if (purposes.length == 0) throw new Error('资金用途暂不可用,请重试')
return purposes
}
export function demandValidationMessage(amount: string, term: string, code: string, detail: string): string {
if (!wanAmountValid(amount) || wanToCents(amount) <= 0) return '请填写大于零的融资金额(万元,最多六位小数)'
if (!/^[1-9][0-9]{0,3}$/.test(term)) return '融资期限请输入1至9999的整数月'
if (code.length == 0) return '请选择资金用途'
if (code == 'other' && (detail.trim().length < 2 || detail.trim().length > 200 || /[<>\x00-\x1f\x7f]/.test(detail))) return '其他用途请填写2至200字说明,不含标签或控制字符'
if (code != 'other' && detail.length > 0) return '仅其他用途需要填写说明'
return ''
}
+24
View File
@@ -0,0 +1,24 @@
// 仅供当前填写页与确认页传递数据,不写本机存储、不发起云端保存。
let currentToken = ''
let currentInput: UTSJSONObject | null = null
let submissionFingerprint = ''
let submissionKey = ''
export function clearAssessmentFlow(): void { currentToken = ''; currentInput = null; submissionFingerprint = ''; submissionKey = '' }
// 只在本次流程内复用幂等键:网络超时可安全重试,修改提交内容时生成新键。
export function assessmentRequestKey(token: string, fingerprint: string): string {
const signature = token + ':' + fingerprint
if (submissionFingerprint != signature || submissionKey.length == 0) {
submissionFingerprint = signature
submissionKey = 'assessment_' + Date.now().toString() + '_' + Math.random().toString(36).substring(2, 14)
}
return submissionKey
}
export function readAssessmentFlow(token: string): UTSJSONObject | null {
if (token.length == 0 || token != currentToken) { clearAssessmentFlow(); return null }
return currentInput
}
export function writeAssessmentFlow(token: string, input: UTSJSONObject): void {
if (token.length == 0) { clearAssessmentFlow(); return }
currentToken = token; currentInput = input
}
+29 -43
View File
@@ -1,5 +1,7 @@
import { getUserStorage, setUserStorage } from '@/services/storage.uts'
import { wanAmountValid, yuanToWan, wanToYuan } from './money.uts'
import { getSession } from './api.uts'
import { readAssessmentFlow, writeAssessmentFlow, clearAssessmentFlow } from './assessment-flow.uts'
export { clearAssessmentFlow } from './assessment-flow.uts'
import { wanAmountValid, wanToYuan, wanToCents } from './money.uts'
export type AssessmentForm = {
name: string, idCard: string, phone: string, city: string, maritalStatus: string,
@@ -7,46 +9,30 @@ export type AssessmentForm = {
housingFund: string, house: string, car: string, savings: string, creditCard: string,
creditLoanCount: string, totalDebtBalance: string, monthlyLoanPayment: string,
creditCardCount: string, creditCardTotalLimit: string, creditCardUsedLimit: string,
otherAssets: string, enterpriseName: string, businessCode: string, annualRevenue: string, operatingYears: string
otherAssets: string, enterpriseName: string, businessCode: string, annualRevenue: string, operatingYears: string, operatingMonths: string,
requestedAmountWan: string, requestedTermMonths: string, purposeCode: string, purposeLabel: string, purposeDetail: string
}
export type SelectedDocument = { name: string, path: string }
export type DataAuthorization = { key: string, name: string, selected: boolean }
export function emptyAssessmentForm(): AssessmentForm {
return { name: '', idCard: '', phone: '', city: '', maritalStatus: '', occupation: '', company: '', monthlyIncome: '', socialSecurity: '', housingFund: '', house: '', car: '', savings: '', creditCard: '', otherAssets: '', creditLoanCount: '', totalDebtBalance: '', monthlyLoanPayment: '', creditCardCount: '', creditCardTotalLimit: '', creditCardUsedLimit: '', enterpriseName: '', businessCode: '', annualRevenue: '', operatingYears: '' }
return { name: '', idCard: '', phone: '', city: '', maritalStatus: '', occupation: '', company: '', monthlyIncome: '', socialSecurity: '', housingFund: '', house: '', car: '', savings: '', creditCard: '', otherAssets: '', creditLoanCount: '', totalDebtBalance: '', monthlyLoanPayment: '', creditCardCount: '', creditCardTotalLimit: '', creditCardUsedLimit: '', enterpriseName: '', businessCode: '', annualRevenue: '', operatingYears: '', operatingMonths: '', requestedAmountWan: '', requestedTermMonths: '', purposeCode: '', purposeLabel: '', purposeDetail: '' }
}
export function draftText(saved: UTSJSONObject, key: string): string {
const value = saved[key]
return typeof value == 'string' ? value as string : ''
}
function flowToken(): string { const session = getSession(); return session == null ? '' : session['token'] as string }
export function activeAssessmentType(): string {
const type = getUserStorage('assessmentActiveType')
if (type == 'enterprise' || type == 'personal') return type as string
const legacy = getUserStorage('assessmentDraft')
if (legacy != null && typeof legacy == 'object' && draftText(legacy as UTSJSONObject, 'type') == 'enterprise') return 'enterprise'
return 'personal'
const current = readAssessmentFlow(flowToken())
return current == null ? 'personal' : draftText(current, 'type')
}
export function loadAssessmentDraft(type: string): UTSJSONObject | null {
const scoped = getUserStorage('assessmentDraft:' + type)
if (scoped != null && typeof scoped == 'object') return normalizeAssessmentDraft(scoped as UTSJSONObject)
const legacy = getUserStorage('assessmentDraft')
if (legacy != null && typeof legacy == 'object') {
const saved = legacy as UTSJSONObject
if (draftText(saved, 'type') == type) return normalizeAssessmentDraft(saved)
}
return null
}
export function normalizeAssessmentDraft(saved: UTSJSONObject): UTSJSONObject {
if (saved['moneyUnit'] == 'wan') return saved
const result: UTSJSONObject = { ...saved, moneyUnit: 'wan', draftVersion: 2 }
// 老草稿只有年营收以万元保存,其余金额均为元;只转换一次。
const keys = ['monthlyIncome', 'totalDebtBalance', 'monthlyLoanPayment', 'creditCardTotalLimit', 'creditCardUsedLimit']
if (saved['moneyUnit'] == 'yuan') keys.push('annualRevenue')
keys.forEach((key) => { result[key] = yuanToWan(draftText(saved, key)) })
return result
export function getAssessmentInput(type: string): UTSJSONObject | null {
const current = readAssessmentFlow(flowToken())
return current != null && current['type'] == type ? current : null
}
export function assessmentApiPayload(saved: UTSJSONObject): UTSJSONObject {
const normalized = normalizeAssessmentDraft(saved)
const normalized = saved
const result: UTSJSONObject = { ...normalized, moneyUnit: 'yuan' }
const keys = ['annualRevenue', 'monthlyIncome', 'totalDebtBalance', 'monthlyLoanPayment', 'creditCardTotalLimit', 'creditCardUsedLimit']
keys.forEach((key) => {
@@ -55,17 +41,18 @@ export function assessmentApiPayload(saved: UTSJSONObject): UTSJSONObject {
})
return result
}
export function formFromDraft(saved: UTSJSONObject): AssessmentForm {
export function formFromInput(saved: UTSJSONObject): AssessmentForm {
return {
name: draftText(saved, 'name'), idCard: draftText(saved, 'idCard'), phone: draftText(saved, 'phone'), city: draftText(saved, 'city'), maritalStatus: draftText(saved, 'maritalStatus'),
occupation: draftText(saved, 'occupation'), company: draftText(saved, 'company'), monthlyIncome: draftText(saved, 'monthlyIncome'), socialSecurity: draftText(saved, 'socialSecurity'), housingFund: draftText(saved, 'housingFund'),
house: ownershipFromDraft(saved, 'house'), car: ownershipFromDraft(saved, 'car'), savings: draftText(saved, 'savings'), creditCard: draftText(saved, 'creditCard'), otherAssets: draftText(saved, 'otherAssets'),
house: ownershipFromInput(saved, 'house'), car: ownershipFromInput(saved, 'car'), savings: draftText(saved, 'savings'), creditCard: draftText(saved, 'creditCard'), otherAssets: draftText(saved, 'otherAssets'),
creditLoanCount: draftText(saved, 'creditLoanCount'), totalDebtBalance: draftText(saved, 'totalDebtBalance'), monthlyLoanPayment: draftText(saved, 'monthlyLoanPayment'),
creditCardCount: draftText(saved, 'creditCardCount'), creditCardTotalLimit: draftText(saved, 'creditCardTotalLimit'), creditCardUsedLimit: draftText(saved, 'creditCardUsedLimit'),
enterpriseName: draftText(saved, 'enterpriseName'), businessCode: draftText(saved, 'businessCode'), annualRevenue: draftText(saved, 'annualRevenue'), operatingYears: draftText(saved, 'operatingYears')
enterpriseName: draftText(saved, 'enterpriseName'), businessCode: draftText(saved, 'businessCode'), annualRevenue: draftText(saved, 'annualRevenue'), operatingYears: draftText(saved, 'operatingYears'), operatingMonths: draftText(saved, 'operatingMonths'),
requestedAmountWan: draftText(saved, 'requestedAmountWan'), requestedTermMonths: draftText(saved, 'requestedTermMonths'), purposeCode: draftText(saved, 'purposeCode'), purposeLabel: draftText(saved, 'purposeLabel'), purposeDetail: draftText(saved, 'purposeDetail')
}
}
export function ownershipFromDraft(saved: UTSJSONObject, key: string): string {
export function ownershipFromInput(saved: UTSJSONObject, key: string): string {
const value = draftText(saved, key)
if (value == '有' || value == '无') return value
if (value == '无房产' || value == '无车辆') return '无'
@@ -83,9 +70,10 @@ export function assetValidationMessage(form: AssessmentForm): string {
for (let i = 0; i < amounts.length; i++) {
if (amounts[i].length > 0 && !wanAmountValid(amounts[i])) return amountLabels[i] + '范围为 0–99999.999999 万元,最多六位小数'
}
if (form.creditCardTotalLimit.length > 0 && form.creditCardUsedLimit.length > 0 && wanToCents(form.creditCardUsedLimit) > wanToCents(form.creditCardTotalLimit)) return '信用卡已使用额度不能大于总额度'
return ''
}
export function documentsFromDraft(saved: UTSJSONObject): SelectedDocument[] {
export function documentsFromInput(saved: UTSJSONObject): SelectedDocument[] {
const results: SelectedDocument[] = []
const items = saved['documents'] as UTSJSONObject[] | null
if (items == null) return results
@@ -95,22 +83,20 @@ export function documentsFromDraft(saved: UTSJSONObject): SelectedDocument[] {
})
return results
}
export function saveAssessmentDraft(type: string, step: number, form: AssessmentForm, documents: SelectedDocument[], authorizations: DataAuthorization[], documentConsent: boolean, dataConsent: boolean): void {
export function setAssessmentInput(type: string, step: number, form: AssessmentForm, documents: SelectedDocument[], authorizations: DataAuthorization[], documentConsent: boolean, dataConsent: boolean): void {
const files: UTSJSONObject[] = documents.map((item): UTSJSONObject => ({ name: item.name, path: item.path }))
const grants: string[] = authorizations.filter((item) => item.selected).map((item) => item.key)
const payload: UTSJSONObject = {
type, step, moneyUnit: 'wan', draftVersion: 2, name: form.name, idCard: form.idCard, phone: form.phone, city: form.city, maritalStatus: form.maritalStatus,
type, step, moneyUnit: 'wan', name: form.name, idCard: form.idCard, phone: form.phone, city: form.city, maritalStatus: form.maritalStatus,
occupation: form.occupation, company: form.company, monthlyIncome: form.monthlyIncome, socialSecurity: form.socialSecurity, housingFund: form.housingFund,
house: form.house, car: form.car, savings: form.savings, creditCard: form.creditCard, otherAssets: form.otherAssets,
creditLoanCount: form.creditLoanCount, totalDebtBalance: form.totalDebtBalance, monthlyLoanPayment: form.monthlyLoanPayment,
creditCardCount: form.creditCardCount, creditCardTotalLimit: form.creditCardTotalLimit, creditCardUsedLimit: form.creditCardUsedLimit,
enterpriseName: form.enterpriseName, businessCode: form.businessCode, annualRevenue: form.annualRevenue, operatingYears: form.operatingYears,
enterpriseName: form.enterpriseName, businessCode: form.businessCode, annualRevenue: form.annualRevenue, operatingYears: form.operatingYears, operatingMonths: form.operatingMonths,
requestedAmountWan: form.requestedAmountWan, requestedTermMonths: form.requestedTermMonths, purposeCode: form.purposeCode, purposeLabel: form.purposeLabel, purposeDetail: form.purposeDetail,
documents: files, authorizations: grants, documentConsent, dataConsent
}
setUserStorage('assessmentActiveType', type)
setUserStorage('assessmentDraft:' + type, payload)
// 保留现有提交与档案入口读取的活动草稿。
setUserStorage('assessmentDraft', payload)
writeAssessmentFlow(flowToken(), payload)
}
export function showAgreementNotice(title: string): void {
uni.showModal({ title, content: '完整协议将在相关服务开放时提供。当前勾选仅保存您的选择,不会发起实名认证、查询外部数据或上传资料。', showCancel: false, confirmText: '我知道了' })
@@ -118,13 +104,13 @@ export function showAgreementNotice(title: string): void {
export type AssessmentProgress = { percent: number, basicDone: boolean, incomeDone: boolean, assetsDone: boolean, authorizationSelected: boolean }
export function assessmentProgress(type: string): AssessmentProgress {
const saved = loadAssessmentDraft(type)
const saved = getAssessmentInput(type)
if (saved == null) return { percent: 0, basicDone: false, incomeDone: false, assetsDone: false, authorizationSelected: false }
const basic = type == 'enterprise' ? ['enterpriseName', 'businessCode', 'name', 'phone', 'city'] : ['name', 'idCard', 'phone', 'city', 'maritalStatus']
const income = type == 'enterprise' ? ['occupation', 'operatingYears', 'annualRevenue'] : ['occupation', 'company', 'monthlyIncome', 'socialSecurity', 'housingFund']
const income = type == 'enterprise' ? ['occupation', 'operatingMonths', 'annualRevenue'] : ['occupation', 'company', 'monthlyIncome', 'socialSecurity', 'housingFund']
const assets = ['house', 'car', 'creditLoanCount', 'totalDebtBalance', 'monthlyLoanPayment', 'creditCardCount', 'creditCardTotalLimit', 'creditCardUsedLimit']
const keys = basic.concat(income).concat(assets)
const filled = keys.filter((key) => draftText(saved, key).trim().length > 0).length
const grants = saved['authorizations'] as string[] | null
return { percent: Math.round(filled / keys.length * 100), basicDone: basic.every((key) => draftText(saved, key).length > 0), incomeDone: income.every((key) => draftText(saved, key).length > 0), assetsDone: assets.every((key) => draftText(saved, key).trim().length > 0) && assetValidationMessage(formFromDraft(saved)).length == 0, authorizationSelected: documentsFromDraft(saved).length > 0 || (grants != null && grants.length > 0) }
return { percent: Math.round(filled / keys.length * 100), basicDone: basic.every((key) => draftText(saved, key).length > 0), incomeDone: income.every((key) => draftText(saved, key).length > 0), assetsDone: assets.every((key) => draftText(saved, key).trim().length > 0) && assetValidationMessage(formFromInput(saved)).length == 0, authorizationSelected: documentsFromInput(saved).length > 0 || (grants != null && grants.length > 0) }
}
+17
View File
@@ -0,0 +1,17 @@
// 只清理指定账户命名空间,不触及其他账户或旧匿名草稿。
export function clearAccountCache(userId: string): void {
if (userId.length == 0) return
const prefix = 'airz:v1:user:' + userId + ':'
uni.getStorageInfoSync().keys.forEach((key: string) => {
if (key.startsWith(prefix)) uni.removeStorageSync(key)
})
}
// 草稿功能已取消,启动时移除旧版本遗留键,不自动导入或恢复敏感表单。
export function clearLegacyAssessmentCache(): void {
uni.getStorageInfoSync().keys.forEach((key: string) => {
if (key == 'assessmentDraft' || key.startsWith('assessmentDraft:') || key.endsWith(':assessmentDraft')
|| key.includes(':assessmentDraft:') || key.endsWith(':assessmentActiveType') || key.endsWith(':assessmentEditStep')
|| key == 'assessmentActiveType' || key == 'assessmentEditStep') uni.removeStorageSync(key)
})
}