diff --git a/App.uvue b/App.uvue
index 560a7d7..f44a10d 100644
--- a/App.uvue
+++ b/App.uvue
@@ -1,6 +1,8 @@
diff --git a/packages/finance/archive/archive.uvue b/packages/finance/archive/archive.uvue
index d73de81..b2b0582 100644
--- a/packages/finance/archive/archive.uvue
+++ b/packages/finance/archive/archive.uvue
@@ -1,65 +1,117 @@
-
-
-
- 个人档案企业档案
- 档案完整度{{ progress.percent }}%{{ progress.percent == 100 ? '基础信息已完善,可继续查看评估方案。' : '完善基础信息,让融资评估更贴近您的情况。' }}
- {{ item.name }}{{ item.note }}{{ item.done ? (item.step == 3 ? '已选择' : '已完善') : (item.step == 3 ? '选填' : '待完善') }}
-
- 草稿保存在当前设备,资料与授权可自主选择。
-
-
+
+
+
+
+ {{ selectedId.length > 0 ? '已提交的评估资料' : '已提交的评估记录' }}
+ 提交后的资料不可修改;再次评估请新建记录。未提交的填写内容不会保存。
+
+
+
+
+ {{ error }}重试加载
+ 正在加载…
+
+ {{ detail!.record.type == 'personal' ? '个人' : '企业' }}评估 · #{{ detail!.record.id }}
+ {{ detail!.record.statusLabel }}
+ {{ detail!.record.message }}
+ 提交时间:{{ detail!.record.submittedAt }}
+ {{ detail!.record.status == 'succeeded' ? '查看分析报告' : '查看分析状态 / 重试' }}
+ {{ field.label }}{{ field.value }}
+ 返回记录列表
+
+
+ 暂无已提交的评估记录
+
+ {{ item.type == 'personal' ? '个人' : '企业' }}评估 · #{{ item.id }}
+ {{ item.statusLabel }}
+ {{ item.city }} · {{ item.submittedAt }}
+ 查看已提交资料
+
+ 加载更多
+ 已显示全部记录
+
+
+
+
-
-
diff --git a/packages/finance/assessment/assessment.uvue b/packages/finance/assessment/assessment.uvue
index fa5b78b..f82642d 100644
--- a/packages/finance/assessment/assessment.uvue
+++ b/packages/finance/assessment/assessment.uvue
@@ -3,7 +3,7 @@
- {{ step + 1 }}/5
+ 未提交内容不会保存,离开或刷新后需重新填写。{{ step + 1 }}/5
融资类型个人企业
{{ assessmentType == 'enterprise' ? '企业信用融资' : '个人信用融资' }} · {{ step == 3 ? '资料与授权均为选填' : '请按实际情况填写' }}
@@ -19,7 +19,7 @@
-
+ 经营月数
年营收(万元)
@@ -29,6 +29,14 @@
+ 资金需求(必填)
+ 填写您期望的金额、期限和用途,不代表机构审批结果。
+ 融资金额万元
+ 融资期限个月
+ 正在加载资金用途…
+ {{ purposeError }}
+
+ 用途说明
贷款信息
@@ -58,41 +66,65 @@
diff --git a/packages/finance/services/api.uts b/packages/finance/services/api.uts
index 80e407d..c8470bd 100644
--- a/packages/finance/services/api.uts
+++ b/packages/finance/services/api.uts
@@ -1,20 +1,10 @@
-import { setUserStorage } from '@/services/storage.uts'
import { API_DEMO_ENABLED, API_BASE_URL, wait, requestJson } from '@/services/api.uts'
import { demoProducts, parseProduct, FinanceProduct } from './finance.uts'
-import { assessmentApiPayload } from '@/services/assessment.uts'
+import { createAssessment } from './records.uts'
export { API_BASE_URL } from '@/services/api.uts'
export async function submitAssessment(payload: UTSJSONObject): Promise {
- const apiPayload = assessmentApiPayload(payload)
- if (API_DEMO_ENABLED) {
- await wait(650)
- setUserStorage('latestAssessment', apiPayload)
- return 'ASSESSMENT-DEMO-001'
- }
- const result = await requestJson('/assessments', 'POST', apiPayload)
- const id = result['id'] as string | null
- if (id == null || id.length == 0) throw new Error('评估提交成功但未返回编号')
- return id
+ return await createAssessment(payload)
}
export async function fetchMatches(): Promise {
diff --git a/packages/finance/services/records.uts b/packages/finance/services/records.uts
new file mode 100644
index 0000000..8ae6203
--- /dev/null
+++ b/packages/finance/services/records.uts
@@ -0,0 +1,170 @@
+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'
+
+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[] }
+
+function recordFrom(item: UTSJSONObject): AssessmentRecord {
+ const submitted = draftText(item, 'submittedAt')
+ 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 }
+}
+
+export async function retryAssessment(id: string, requestId: string): Promise {
+ 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 async function fetchAnalysisReport(id: string): Promise {
+ 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 facts: AssessmentField[] = []
+ const rawFacts = result['facts'] as UTSJSONObject[]
+ rawFacts.forEach((fact: UTSJSONObject) => {
+ const raw = fact['value']
+ let value = raw == null ? '未填写' : (typeof raw == 'number' ? (raw as number).toString() : draftText(fact, 'value'))
+ if (fact['value'] != null && fact['unit'] == 'yuan') value = yuanToWan(value) + ' 万元'
+ if (fact['value'] != null && fact['unit'] == 'month') value += ' 个月'
+ if (fact['state'] == 'invalid') value = '格式需核实'
+ const enumLabels: UTSJSONObject = { six_months_plus: '连续缴纳6个月以上', under_six_months: '缴纳不足6个月', none: '无' }
+ if (enumLabels[value] != null) value = enumLabels[value] as string
+ // v2的需求在独立区块展示,避免用途代码和同一金额重复出现。
+ if (version == 2 && ['requestedAmountYuan', 'requestedTermMonths', 'purposeCode'].includes(draftText(fact, 'fieldKey'))) return
+ facts.push({ label: draftText(fact, 'label'), value } as AssessmentField)
+ })
+ if (version == 2) {
+ const demandData = result['demand'] as UTSJSONObject
+ const completeness = result['completeness'] as UTSJSONObject
+ const readiness = result['readiness'] as UTSJSONObject
+ const items = completeness['items'] as UTSJSONObject[]
+ 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[] = [
+ { label: '融资金额', value: yuanToWan(draftText(demandData, 'requestedAmountYuan')) + ' 万元' },
+ { label: '融资期限', value: (demandData['requestedTermMonths'] as number).toString() + ' 个月' },
+ { label: '资金用途', value: draftText(demandData, 'purposeLabel') }
+ ]
+ const evaluated = completeness['status'] == 'evaluated'
+ 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 => {
+ const labels = (item['evidenceRefs'] as string[]).map((ref: string): string => {
+ if (ref.startsWith('fact:')) {
+ const fact = rawFacts.find((entry: UTSJSONObject): boolean => draftText(entry, 'fieldKey') == ref.slice(5))
+ return fact == null ? '' : draftText(fact, 'label')
+ }
+ const entry = basis.find((entry: UTSJSONObject): boolean => draftText(entry, 'basisId') == ref.slice(10))
+ return entry == null ? '' : draftText(entry, 'message')
+ }).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'),
+ disclaimer: draftText(result, 'disclaimer'), matchingMessage: '产品资料及匹配规则尚未就绪,本报告未执行产品匹配。', modelCode: draftText(result, 'modelCode'),
+ demand, completenessText, completenessItems, readinessLabel: readiness['status'] == 'evaluated' ? draftText(readiness, 'label') : '未评定',
+ readinessBasis: basis.map((item: UTSJSONObject): string => draftText(item, 'message')), warnings: result['systemWarnings'] as string[] }
+ }
+ const observations = (result['observations'] as UTSJSONObject[]).map((item: UTSJSONObject): string => {
+ 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: [] }
+}
+
+function enumValue(value: string, labels: string[], codes: string[]): string | null {
+ if (value.length == 0) return null
+ const index = labels.indexOf(value)
+ if (index < 0) throw new Error('选项已变化,请重新选择')
+ return codes[index]
+}
+
+export async function createAssessment(payload: UTSJSONObject): Promise {
+ 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('资料上传与外部查询授权尚未开放,请移除资料及授权选择后提交')
+ const values = assessmentApiPayload(payload)
+ const form = new UTSJSONObject()
+ const textKeys = ['name', 'idCard', 'phone', 'city', 'occupation', 'company', 'enterpriseName', 'businessCode']
+ const moneyKeys = ['monthlyIncome', 'annualRevenue', 'totalDebtBalance', 'monthlyLoanPayment', 'creditCardTotalLimit', 'creditCardUsedLimit']
+ textKeys.concat(moneyKeys).forEach((key: string) => { const value = draftText(values, key).trim(); form[key] = value.length == 0 ? null : value })
+ if (form['idCard'] != null) form['idCard'] = (form['idCard'] as string).toUpperCase()
+ if (form['businessCode'] != null) form['businessCode'] = (form['businessCode'] as string).toUpperCase()
+ const countKeys = ['creditLoanCount', 'creditCardCount', 'operatingMonths']
+ countKeys.forEach((key: string) => {
+ const value = draftText(values, key)
+ if (value.length > 0 && !/^[0-9]{1,4}$/.test(value)) throw new Error('数量与经营月数须为0至9999的整数')
+ form[key] = value.length == 0 ? null : parseInt(value)
+ })
+ form['maritalStatus'] = enumValue(draftText(values, 'maritalStatus'), ['已婚', '未婚', '离异', '丧偶'], ['married', 'single', 'divorced', 'widowed'])
+ const socialKeys = ['socialSecurity', 'housingFund']
+ socialKeys.forEach((key: string) => { form[key] = enumValue(draftText(values, key), ['连续缴纳 6 个月以上', '缴纳不足 6 个月', '未缴纳'], ['six_months_plus', 'under_six_months', 'none']) })
+ const ownershipKeys = ['house', 'car']
+ ownershipKeys.forEach((key: string) => { form[key] = enumValue(draftText(values, key), ['有', '无'], ['yes', 'no']) })
+ const cityNames = ['深圳市', '广州市', '北京市', '上海市', '杭州市']
+ 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)
+ const id = draftText(result, 'assessmentId')
+ if (id.length == 0) throw new Error('提交回执不完整,请重试')
+ return id
+}
+
+export async function fetchAssessments(cursor: string): Promise {
+ const result = await requestJson('/assessments', 'GET', { limit: '20', cursor })
+ 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') }
+}
+
+export async function fetchAssessmentStatus(id: string): Promise {
+ return recordFrom(await requestJson('/assessments/' + encodeURIComponent(id) + '/status', 'GET', {}))
+}
+
+export async function fetchAssessmentDetail(id: string): Promise {
+ const result = await requestJson('/assessments/' + encodeURIComponent(id), 'GET', {})
+ const form = result['form'] as UTSJSONObject | null
+ if (form == null) throw new Error('评估详情不完整')
+ const fields: AssessmentField[] = []
+ 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)
+ 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)
+ 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: '未缴纳' }
+ keys.forEach((key: string, index: number) => {
+ const raw = form[key]
+ let value = raw == null ? '未填写' : (typeof raw == 'number' ? (raw as number).toString() : raw as string)
+ if (raw != null && moneyKeys.includes(key)) value = yuanToWan(value) + ' 万元'
+ if (raw != null && key == 'operatingMonths') value += ' 个月'
+ if (['maritalStatus', 'socialSecurity', 'housingFund', 'house', 'car'].includes(key) && enumLabels[value] != null) value = enumLabels[value] as string
+ 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)
+ })
+ return { record: recordFrom(result), fields }
+}
diff --git a/services/api.uts b/services/api.uts
index 413aa00..0bf1412 100644
--- a/services/api.uts
+++ b/services/api.uts
@@ -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 {
+ return new Promise((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(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('头像上传失败,请检查网络后重试')) }
+ })
+ })
+}
diff --git a/services/assessment-demand.uts b/services/assessment-demand.uts
new file mode 100644
index 0000000..f739842
--- /dev/null
+++ b/services/assessment-demand.uts
@@ -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 {
+ 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 ''
+}
diff --git a/services/assessment-flow.uts b/services/assessment-flow.uts
new file mode 100644
index 0000000..b8b3962
--- /dev/null
+++ b/services/assessment-flow.uts
@@ -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
+}
diff --git a/services/assessment.uts b/services/assessment.uts
index d97d9ff..95084a5 100644
--- a/services/assessment.uts
+++ b/services/assessment.uts
@@ -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) }
}
diff --git a/services/cache-lifecycle.uts b/services/cache-lifecycle.uts
new file mode 100644
index 0000000..95ab321
--- /dev/null
+++ b/services/cache-lifecycle.uts
@@ -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)
+ })
+}