refactor: 重构存储与接口适配,统一金额单位与会话管理

- 新增配置、存储、金额转换工具服务
- 按userId隔离本地缓存,替换旧设备级存储
- 统一移动端金额单位为万元,新增转换适配逻辑
- 重构认证与请求逻辑,支持真实后端会话
- 调整搜索、帮助、融资模块的模拟开关与数据适配
- 更新文档说明开发状态与配置规则
This commit is contained in:
hujun
2026-09-22 17:25:52 +08:00
parent 4a69357606
commit dcf12a4471
25 changed files with 1619 additions and 143 deletions
+59 -38
View File
@@ -1,56 +1,77 @@
/** Shared request, configuration and session infrastructure. */
export const API_BASE_URL = ''
import { API_BASE_URL, APP_ENVIRONMENT } from './config.uts'
export { API_BASE_URL, API_DEMO_ENABLED } from './config.uts'
const REQUEST_TIMEOUT = 10000
const SESSION_KEY = 'aiFinanceSession'
const wait = (duration: number) => new Promise<void>((resolve) => {
setTimeout(() => resolve(), duration)
})
const wait = (duration: number) => new Promise<void>((resolve) => { setTimeout(() => resolve(), duration) })
export function saveSession(token: string, phone: string): void {
uni.setStorageSync(SESSION_KEY, { token, phone, loggedInAt: Date.now() })
export function saveSession(token: string, phone: string, userId: string, expiresAt: string): void {
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('登录凭证已过期')
uni.setStorageSync(SESSION_KEY, { token, phone, userId, expiresAt, loggedInAt: Date.now() })
}
export function getSession(): UTSJSONObject | null {
const value = uni.getStorageSync(SESSION_KEY)
if (value == null || typeof value != 'object') return null
return value as UTSJSONObject
const value = uni.getStorageSync(SESSION_KEY)
if (value == null || typeof value != 'object') return null
const session = value as UTSJSONObject
const token = session['token'] as string | 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)
return null
}
const expiry = Date.parse(expiresAt)
if (isNaN(expiry) || expiry <= Date.now()) { uni.removeStorageSync(SESSION_KEY); return null }
return session
}
export function isLoggedIn(): boolean {
return getSession() != null
}
export function clearSession(): void {
uni.removeStorageSync(SESSION_KEY)
}
export function isLoggedIn(): boolean { return getSession() != null }
export function clearSession(): void { 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 => {
if (error == null) return fallback
const text = error.toString()
return text.length > 0 ? text : fallback
if (error == null) return fallback
if (error instanceof Error) return error.message
const text = error.toString()
return text.length > 0 ? text : fallback
}
const requestJson = (path: string, method: RequestMethod, payload: UTSJSONObject): Promise<UTSJSONObject> => {
return new Promise<UTSJSONObject>((resolve, reject) => {
uni.request({
url: API_BASE_URL + path,
method,
data: payload,
timeout: REQUEST_TIMEOUT,
success: (response) => {
if (response.statusCode >= 200 && response.statusCode < 300) {
if (response.data == null) resolve(new UTSJSONObject())
else resolve(response.data as UTSJSONObject)
} else {
reject('请求失败(' + response.statusCode.toString() + ')')
}
},
fail: (error) => {
reject(apiMessage(error, '网络请求失败,请稍后重试'))
}
})
})
return new Promise<UTSJSONObject>((resolve, reject) => {
if (API_BASE_URL.length == 0 || (APP_ENVIRONMENT == 'production' && !API_BASE_URL.startsWith('https://'))) {
reject(new Error('服务地址尚未配置')); return
}
const sentToken = currentToken()
const header: UTSJSONObject = { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }
if (sentToken.length > 0) header['Authorization'] = 'Bearer ' + sentToken
uni.request({
url: API_BASE_URL + path, method, data: payload, header, timeout: REQUEST_TIMEOUT,
success: (response) => {
// 旧请求不得覆盖刚登录的另一账户,也不能因旧401清除新会话。
if (currentToken() != sentToken) { reject(new Error('登录状态已变化,请重新操作')); return }
if (response.statusCode == 401 && sentToken.length > 0) clearSession()
if (response.data == null || typeof response.data != 'object') {
reject(new Error('服务响应格式错误')); return
}
const body = response.data as UTSJSONObject
const code = body['code'] as number | null
const message = body['msg'] as string | null
if (response.statusCode < 200 || response.statusCode >= 300 || code != 1) {
if (code == 401 && sentToken.length > 0) clearSession()
reject(new Error(message != null && message.length > 0 ? message : '请求失败,请稍后重试')); return
}
const data = body['data']
if (data == null) resolve(new UTSJSONObject())
else if (typeof data == 'object') resolve(data as UTSJSONObject)
else reject(new Error('服务响应数据格式错误'))
},
fail: () => { reject(new Error('网络连接失败,请稍后重试')) }
})
})
}
export { apiMessage, requestJson, wait }
+33 -11
View File
@@ -1,3 +1,6 @@
import { getUserStorage, setUserStorage } from '@/services/storage.uts'
import { wanAmountValid, yuanToWan, wanToYuan } from './money.uts'
export type AssessmentForm = {
name: string, idCard: string, phone: string, city: string, maritalStatus: string,
occupation: string, company: string, monthlyIncome: string, socialSecurity: string,
@@ -17,22 +20,41 @@ export function draftText(saved: UTSJSONObject, key: string): string {
return typeof value == 'string' ? value as string : ''
}
export function activeAssessmentType(): string {
const type = uni.getStorageSync('assessmentActiveType')
const type = getUserStorage('assessmentActiveType')
if (type == 'enterprise' || type == 'personal') return type as string
const legacy = uni.getStorageSync('assessmentDraft')
const legacy = getUserStorage('assessmentDraft')
if (legacy != null && typeof legacy == 'object' && draftText(legacy as UTSJSONObject, 'type') == 'enterprise') return 'enterprise'
return 'personal'
}
export function loadAssessmentDraft(type: string): UTSJSONObject | null {
const scoped = uni.getStorageSync('assessmentDraft:' + type)
if (scoped != null && typeof scoped == 'object') return scoped as UTSJSONObject
const legacy = uni.getStorageSync('assessmentDraft')
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 saved
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 assessmentApiPayload(saved: UTSJSONObject): UTSJSONObject {
const normalized = normalizeAssessmentDraft(saved)
const result: UTSJSONObject = { ...normalized, moneyUnit: 'yuan' }
const keys = ['annualRevenue', 'monthlyIncome', 'totalDebtBalance', 'monthlyLoanPayment', 'creditCardTotalLimit', 'creditCardUsedLimit']
keys.forEach((key) => {
const value = draftText(normalized, key)
result[key] = value.length > 0 ? wanToYuan(value) : ''
})
return result
}
export function formFromDraft(saved: UTSJSONObject): AssessmentForm {
return {
name: draftText(saved, 'name'), idCard: draftText(saved, 'idCard'), phone: draftText(saved, 'phone'), city: draftText(saved, 'city'), maritalStatus: draftText(saved, 'maritalStatus'),
@@ -59,7 +81,7 @@ export function assetValidationMessage(form: AssessmentForm): string {
const amounts = [form.totalDebtBalance, form.monthlyLoanPayment, form.creditCardTotalLimit, form.creditCardUsedLimit]
const amountLabels = ['欠款总余额', '贷款月还款金额', '总额度', '已使用总额度']
for (let i = 0; i < amounts.length; i++) {
if (amounts[i].length > 0 && !/^[0-9]{1,12}([.][0-9]{1,2})?$/.test(amounts[i])) return amountLabels[i] + '请输入有效金额,最多两位小数'
if (amounts[i].length > 0 && !wanAmountValid(amounts[i])) return amountLabels[i] + '范围为 0–99999.999999 万元,最多六位小数'
}
return ''
}
@@ -77,7 +99,7 @@ export function saveAssessmentDraft(type: string, step: number, form: Assessment
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, name: form.name, idCard: form.idCard, phone: form.phone, city: form.city, maritalStatus: form.maritalStatus,
type, step, moneyUnit: 'wan', draftVersion: 2, 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,
@@ -85,10 +107,10 @@ export function saveAssessmentDraft(type: string, step: number, form: Assessment
enterpriseName: form.enterpriseName, businessCode: form.businessCode, annualRevenue: form.annualRevenue, operatingYears: form.operatingYears,
documents: files, authorizations: grants, documentConsent, dataConsent
}
uni.setStorageSync('assessmentActiveType', type)
uni.setStorageSync('assessmentDraft:' + type, payload)
setUserStorage('assessmentActiveType', type)
setUserStorage('assessmentDraft:' + type, payload)
// 保留现有提交与档案入口读取的活动草稿。
uni.setStorageSync('assessmentDraft', payload)
setUserStorage('assessmentDraft', payload)
}
export function showAgreementNotice(title: string): void {
uni.showModal({ title, content: '完整协议将在相关服务开放时提供。当前勾选仅保存您的选择,不会发起实名认证、查询外部数据或上传资料。', showCancel: false, confirmText: '我知道了' })
+7
View File
@@ -0,0 +1,7 @@
// 发布前选择目标环境并配置 HTTPS 地址;未配置时请求失败,不回退模拟服务。
export const APP_ENVIRONMENT: string = 'development' as string
const DEVELOPMENT_API_URL = 'http://127.0.0.1/api/v1'
const TEST_API_URL = ''
const PRODUCTION_API_URL = ''
export const API_BASE_URL = APP_ENVIRONMENT == 'production' ? PRODUCTION_API_URL : (APP_ENVIRONMENT == 'test' ? TEST_API_URL : DEVELOPMENT_API_URL)
export const API_DEMO_ENABLED = false
+30
View File
@@ -0,0 +1,30 @@
// 金额转换使用整数分,避免万元与元之间的浮点乘法误差。
export function wanAmountValid(value: string): boolean {
if (!/^[0-9]{1,5}([.][0-9]{1,6})?$/.test(value)) return false
return wanToCents(value) <= 99999999999
}
export function wanToCents(value: string): number {
const parts = value.split('.')
const fraction = parts.length > 1 ? parts[1] : ''
return parseInt(parts[0]) * 1000000 + parseInt((fraction + '000000').substring(0, 6))
}
export function wanToYuan(value: string): string {
if (!wanAmountValid(value)) throw new Error('金额范围为 0–99999.999999 万元,最多六位小数')
const cents = wanToCents(value)
const fraction = (cents % 100).toString()
return Math.floor(cents / 100).toString() + '.' + (fraction.length == 1 ? '0' + fraction : fraction)
}
export function yuanToWan(value: string): string {
if (value.length == 0) return ''
if (!/^[0-9]+([.][0-9]{1,2})?$/.test(value)) return value
const parts = value.split('.')
const fraction = parts.length > 1 ? parts[1] : ''
const digits = parts[0] + (fraction + '00').substring(0, 2)
const padded = '000000' + digits
const whole = parseInt(padded.substring(0, padded.length - 6)).toString()
const decimal = padded.substring(padded.length - 6).replace(/0+$/, '')
return whole + (decimal.length > 0 ? '.' + decimal : '')
}
+11
View File
@@ -0,0 +1,11 @@
import { getSession } from './api.uts'
// 旧的设备级缓存不自动归给新登录用户;按稳定userId隔离,匿名使用独立空间。
export function userStorageKey(key: string): string {
const session = getSession()
const owner = session == null ? 'guest' : 'user:' + (session['userId'] as string)
return 'airz:v1:' + owner + ':' + key
}
export function getUserStorage(key: string): any | null { return uni.getStorageSync(userStorageKey(key)) }
export function setUserStorage(key: string, value: any): void { uni.setStorageSync(userStorageKey(key), value) }
export function removeUserStorage(key: string): void { uni.removeStorageSync(userStorageKey(key)) }