2026-09-30 19:11:44 +08:00
|
|
|
|
import { revisionParams } from './records.uts'
|
2026-09-28 14:56:46 +08:00
|
|
|
|
import { requestJson } from '@/services/api.uts'
|
2026-10-14 14:53:13 +08:00
|
|
|
|
import { API_BASE_URL } from '@/services/config.uts'
|
2026-09-28 14:56:46 +08:00
|
|
|
|
import { yuanToWan } from '@/services/money.uts'
|
|
|
|
|
|
|
2026-10-14 14:53:13 +08:00
|
|
|
|
export type ProductMaterial = { name: string, note: string, required: boolean, objects: string }
|
|
|
|
|
|
export type ProductView = { logo: string, materialList: ProductMaterial[], consultPhone: string, repayment: string, channel: string, approval: string, 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[] }
|
2026-09-28 14:56:46 +08:00
|
|
|
|
export type MatchReason = { id: string, text: string, evidence: string[], fields: string[], explanation: string }
|
2026-09-30 19:11:44 +08:00
|
|
|
|
export type MatchDetail = { assessmentId: string, revisionId: string, resultVersion: number, snapshotId: string, ruleVersion: string, orderingVersion: string, item: MatchView }
|
2026-10-14 14:53:13 +08:00
|
|
|
|
export type MatchView = { score: number | null, 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 }
|
2026-09-30 19:11:44 +08:00
|
|
|
|
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 }
|
2026-09-28 14:56:46 +08:00
|
|
|
|
|
|
|
|
|
|
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) }
|
2026-10-14 14:53:13 +08:00
|
|
|
|
export function eligibilityLabel(value: string): string { return value == 'eligible' ? '符合已知条件' : (value == 'ineligible' ? '条件不符' : '待确认') }
|
|
|
|
|
|
export function hasProductValue(value: string): boolean { return value.trim().length > 0 && !['未提供', '待核实', '—'].includes(value.trim()) }
|
|
|
|
|
|
export function matchPercentage(state: string, score: number | null): string { return state == 'eligible' && score != null ? score.toString() + '%' : '' }
|
|
|
|
|
|
const fieldNames: UTSJSONObject = { requestedAmountYuan: '融资金额', requestedTermMonths: '融资期限', applicantType: '申请主体', purposeCode: '资金用途', cityCode: '所在城市', monthlyIncome: '月收入', annualRevenue: '年营收', operatingMonths: '经营月数', totalDebtBalance: '贷款余额', monthlyLoanPayment: '每月还款额', creditLoanCount: '贷款笔数', creditCardCount: '信用卡数量', creditCardTotalLimit: '信用卡总额度', creditCardUsedLimit: '信用卡已用额度', socialSecurity: '社保情况', housingFund: '公积金情况', house: '房产情况', car: '车辆情况', establishedMonths: '企业成立月数', annualInvoiceAmount: '年开票金额', taxPaymentMonths: '连续纳税月数', taxpayerGrade: '纳税等级', enterpriseCreditStatus: '企业信用状态', annualRevenueGrowthPercent: '年营收增长率', debtRatioPercent: '负债率', cashFlowStability: '经营流水稳定性', industryCategory: '行业类别' }
|
|
|
|
|
|
const labels: UTSJSONObject = { personal: '个人', enterprise: '企业', daily_consumption: '日常消费', education: '教育进修', home_improvement: '家居装修', personal_business: '个体经营周转', working_capital: '经营周转', inventory_purchase: '采购备货', equipment_purchase: '设备购置', business_expansion: '经营扩展', other: '其他用途', none: '无', under_six_months: '不足六个月', six_months_plus: '六个月及以上', yes: '有', no: '无', normal: '正常', abnormal: '异常', stable: '稳定', unstable: '不稳定', manufacturing: '制造业', trade: '贸易', services: '服务业', construction: '建筑业', agriculture: '农业', finance: '金融业', real_estate: '房地产业', '440300': '深圳市', '440100': '广州市', '110100': '北京市', '310100': '上海市', '330100': '杭州市' }
|
|
|
|
|
|
const moneyFields = ['requestedAmountYuan','monthlyIncome','annualRevenue','totalDebtBalance','monthlyLoanPayment','creditCardTotalLimit','creditCardUsedLimit','annualInvoiceAmount']
|
2026-09-28 14:56:46 +08:00
|
|
|
|
|
|
|
|
|
|
function valueText(value: any | null, field: string): string {
|
|
|
|
|
|
if (value == null) return '未提供'
|
|
|
|
|
|
const raw = value.toString()
|
|
|
|
|
|
if (moneyFields.includes(field)) return yuanToWan(raw) + ' 万元'
|
2026-10-14 14:53:13 +08:00
|
|
|
|
if (field == 'requestedTermMonths' || field == 'operatingMonths' || field == 'establishedMonths' || field == 'taxPaymentMonths') return raw + ' 个月'
|
|
|
|
|
|
if (field == 'debtRatioPercent' || field == 'annualRevenueGrowthPercent') return raw + '%'
|
2026-09-28 14:56:46 +08:00
|
|
|
|
return labels.getString(raw) ?? raw
|
|
|
|
|
|
}
|
|
|
|
|
|
function ruleText(rule: UTSJSONObject): string {
|
|
|
|
|
|
const field = text(rule, 'field'); const op = text(rule, 'operator')
|
|
|
|
|
|
const prefix = fieldNames.getString(field) ?? '条件'
|
|
|
|
|
|
let expected = ''
|
|
|
|
|
|
if (op == 'in' || op == 'between') {
|
|
|
|
|
|
const values = rule['value'] as any[]
|
|
|
|
|
|
expected = values.map((value: any): string => valueText(value, field)).join(op == 'between' ? ' 至 ' : '、')
|
|
|
|
|
|
} else expected = valueText(rule['value'], field)
|
|
|
|
|
|
const operation = op == 'gte' ? '不少于 ' : (op == 'lte' ? '不超过 ' : (op == 'in' ? '属于 ' : ''))
|
|
|
|
|
|
return prefix + ':' + operation + expected + (text(rule, 'requiredSource') == 'verified' ? '(需核验依据)' : '')
|
|
|
|
|
|
}
|
2026-10-14 14:53:13 +08:00
|
|
|
|
function rateText(value: string): string { return value.indexOf('.') >= 0 ? value.replace(/0+$/, '').replace(/\.$/, '') : value }
|
|
|
|
|
|
function validityText(data: UTSJSONObject): string {
|
|
|
|
|
|
const from = text(data, 'validFrom'); const until = text(data, 'validUntil')
|
|
|
|
|
|
if (from.length == 0) return until.length == 0 ? '未设置有效期' : '有效至 ' + until
|
|
|
|
|
|
return until.length == 0 ? from + ' 起生效' : from + ' 至 ' + until
|
|
|
|
|
|
}
|
|
|
|
|
|
function rangeText(min: string, max: string, unit: string): string {
|
|
|
|
|
|
if (min.length == 0) return max.length == 0 ? '未提供' : '最高 ' + max + unit
|
|
|
|
|
|
if (max.length == 0) return '最低 ' + min + unit
|
|
|
|
|
|
return (min == max ? min : min + '–' + max) + unit
|
|
|
|
|
|
}
|
2026-09-28 14:56:46 +08:00
|
|
|
|
function productView(data: UTSJSONObject, publicMode: boolean): ProductView {
|
|
|
|
|
|
const source = data['source'] as UTSJSONObject
|
|
|
|
|
|
const objects = data['applicantTypes'] as string[]; const regions = data['regions'] as string[]
|
|
|
|
|
|
const conditions: string[] = []
|
|
|
|
|
|
if (publicMode) {
|
|
|
|
|
|
const groups = data['rulesByApplicantType'] as UTSJSONObject
|
|
|
|
|
|
objects.forEach((type: string) => {
|
|
|
|
|
|
(groups[type] as UTSJSONObject[]).forEach((r: UTSJSONObject) => { conditions.push((labels.getString(type) ?? type) + ' · ' + ruleText(r)) })
|
|
|
|
|
|
})
|
|
|
|
|
|
} else (data['rules'] as UTSJSONObject[]).forEach((r: UTSJSONObject) => { conditions.push(ruleText(r)) })
|
2026-10-14 14:53:13 +08:00
|
|
|
|
const rawLogo = text(data, 'bankLogo')
|
|
|
|
|
|
const logo = /^https:\/\/[^\s<>"']+$/.test(rawLogo) ? rawLogo : (/^\/uploads\/[a-zA-Z0-9/_.-]+$/.test(rawLogo) ? API_BASE_URL.replace(/\/api\/v1\/?$/, '') + rawLogo : '')
|
|
|
|
|
|
const materialList = ((data['materials'] as UTSJSONObject[] | null) ?? []).map((item: UTSJSONObject): ProductMaterial => {
|
|
|
|
|
|
return { name: text(item, 'name'), note: text(item, 'note'), required: item.getBoolean('required') ?? false, objects: ((item['applicantTypes'] as string[] | null) ?? []).map((v: string): string => labels.getString(v) ?? v).join(' / ') }
|
|
|
|
|
|
})
|
|
|
|
|
|
const rawPhone = text(data, 'consultPhone')
|
|
|
|
|
|
return { logo, materialList, consultPhone: /^[+0-9][0-9 -]{4,29}$/.test(rawPhone) ? rawPhone : '', repayment: text(data, 'repaymentMethod') || '未提供', channel: text(data, 'channel') || '未提供', approval: text(data, 'approvalDuration') || '未提供', id: text(data, 'productId'), version: data.getNumber('productVersion') ?? 0, name: text(data, 'productName'), bank: text(data, 'bankName'), type: text(data, 'productType'), demo: data.getBoolean('isDemo') ?? false,
|
|
|
|
|
|
amount: text(data, 'expectation') || rangeText(text(data, 'amountMinYuan').length == 0 ? '' : yuanToWan(text(data, 'amountMinYuan')), text(data, 'amountMaxYuan').length == 0 ? '' : yuanToWan(text(data, 'amountMaxYuan')), ' 万元'),
|
|
|
|
|
|
rate: rangeText(rateText(text(data, 'annualRateMinPercent')), rateText(text(data, 'annualRateMaxPercent')), '%'),
|
|
|
|
|
|
term: text(data, 'quota') || rangeText(data.getNumber('termMinMonths')?.toString() ?? '', data.getNumber('termMaxMonths')?.toString() ?? '', ' 个月'),
|
|
|
|
|
|
objects: objects.map((v: string): string => labels.getString(v) ?? v).join(' / '), regions: text(data, 'regionMode') == 'unverified' ? '待核实' : (regions.length == 0 ? '全国' : regions.map((v: string): string => labels.getString(v) ?? v).join('、')),
|
|
|
|
|
|
valid: validityText(data), materials: text(data, 'information').length > 0 ? text(data, 'information') : '未提供',
|
2026-09-30 19:11:44 +08:00
|
|
|
|
// 内部引用用于定位资料来源,不把测试环境标识当作面向用户的产品说明。
|
2026-10-14 14:53:13 +08:00
|
|
|
|
source: [text(source, 'publisher'), text(source, 'title')].filter((value: string): boolean => value.length > 0).join(' · ') || '待核实', sourceRef: text(source, 'reference').startsWith('demo:airz-') ? '产品规则档案' : (text(source, 'reference') || '未提供'), checkedOn: text(source, 'checkedOn') || '未提供', conditions }
|
2026-09-28 14:56:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
function matchView(data: UTSJSONObject): MatchView {
|
2026-10-07 10:01:05 +08:00
|
|
|
|
const snapshot = data['productSnapshot'] as UTSJSONObject
|
|
|
|
|
|
const productId = text(data, 'productId'); const version = data.getNumber('productVersion') ?? 0
|
|
|
|
|
|
if (!validRecordId(productId) || text(snapshot, 'productId') != productId || version < 1 || Math.floor(version) != version || snapshot.getNumber('productVersion') != version || !['eligible','ineligible','unknown'].includes(text(data, 'eligibility')) || !['available','unavailable','unknown'].includes(text(data, 'currentAvailability'))) throw new Error('产品冻结资料不完整,请重新加载。')
|
2026-09-28 14:56:46 +08:00
|
|
|
|
const evidence = data['evidence'] as UTSJSONObject[]
|
|
|
|
|
|
const ai = data['aiExplanation'] as UTSJSONObject
|
|
|
|
|
|
const explanations = (ai['reasonExplanations'] as UTSJSONObject[] | null) ?? []
|
|
|
|
|
|
const reasons = (key: string): MatchReason[] => (data[key] as UTSJSONObject[]).map((r: UTSJSONObject): MatchReason => {
|
|
|
|
|
|
const reasonFields: string[] = []
|
2026-10-14 14:53:13 +08:00
|
|
|
|
const reasonText = ['AI_CONDITION_PASS', 'AI_CONDITION_FAIL'].includes(text(r, 'code')) ? text(r, 'text').replace('(根据自填资料判断)', '') : text(r, 'text')
|
2026-09-28 14:56:46 +08:00
|
|
|
|
const lines = (r['evidenceRefs'] as string[]).map((ref: string): string => {
|
|
|
|
|
|
const e = evidence.find((item: UTSJSONObject): boolean => text(item, 'evidenceId') == ref)
|
|
|
|
|
|
if (e == null) return '证据暂不可读取'
|
|
|
|
|
|
const fields = e['fieldRefs'] as string[]; const field = fields.length > 0 ? fields[0] : ''
|
|
|
|
|
|
fields.forEach((name: string) => { if (!reasonFields.includes(name)) reasonFields.push(name) })
|
2026-10-14 14:53:13 +08:00
|
|
|
|
if (text(e, 'source') == 'ai_self_reported') {
|
|
|
|
|
|
if (e['actual'] != null && fieldNames.getString(field) != null) return fieldNames.getString(field)! + ':' + valueText(e['actual'], field)
|
|
|
|
|
|
const names = fields.map((name: string): string => fieldNames.getString(name) ?? '').filter((name: string): boolean => name.length > 0)
|
|
|
|
|
|
return names.length > 0 ? '关联资料:' + names.join('、') : reasonText
|
|
|
|
|
|
}
|
2026-09-28 14:56:46 +08:00
|
|
|
|
const source = text(e, 'source') == 'verified' ? '外部核验' : (text(e, 'source') == 'product_snapshot' ? '产品快照' : (text(e, 'source') == 'self_reported' ? '用户自填,未经核验' : '来源未知'))
|
|
|
|
|
|
return (fieldNames.getString(field) ?? field) + ':' + valueText(e['actual'], field) + ' · ' + source
|
|
|
|
|
|
})
|
|
|
|
|
|
const explanation = explanations.find((item: UTSJSONObject): boolean => text(item, 'reasonId') == text(r, 'reasonId'))
|
2026-10-14 14:53:13 +08:00
|
|
|
|
return { id: text(r, 'reasonId'), text: reasonText, evidence: lines, fields: reasonFields, explanation: text(ai, 'status') == 'completed' && explanation != null ? text(explanation, 'text') : '' }
|
2026-09-28 14:56:46 +08:00
|
|
|
|
})
|
|
|
|
|
|
const availability = text(data, 'currentAvailability')
|
2026-10-14 14:53:13 +08:00
|
|
|
|
const score = data.getNumber('score')
|
|
|
|
|
|
if (score != null && (Math.floor(score) != score || score < 0 || score > 100 || text(data, 'scorePolicyVersion') != 'product-soft-score-v1')) throw new Error('匹配度资料无效,请重新加载。')
|
|
|
|
|
|
const pending = evidence.filter((item: UTSJSONObject): boolean => text(item, 'kind') == 'hard' && text(item, 'outcome') == 'unknown')
|
|
|
|
|
|
const missingData = text(data, 'eligibility') == 'unknown' && pending.length > 0 && pending.every((item: UTSJSONObject): boolean => ['FACT_MISSING', 'AI_CONDITION_UNKNOWN'].includes(text(item, 'reasonCode')))
|
|
|
|
|
|
return { score, id: text(data, 'productId'), state: text(data, 'eligibility'), stateLabel: missingData ? '资料不足' : eligibilityLabel(text(data, 'eligibility')), availability,
|
|
|
|
|
|
availabilityLabel: availability == 'available' ? '当前产品可查看' : (availability == 'unavailable' ? '当前产品已下架' : '当前产品状态暂无法确认'),
|
2026-09-28 14:56:46 +08:00
|
|
|
|
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解释。'),
|
2026-09-30 19:11:44 +08:00
|
|
|
|
aiStatus: text(ai, 'status'), aiCanRetry: ai.getBoolean('canRetry') ?? false, aiCanRequest: data.getBoolean('canRequestExplanation') ?? false, aiGeneratedAt: text(ai, 'generatedAt') }
|
2026-09-28 14:56:46 +08:00
|
|
|
|
}
|
2026-09-30 19:11:44 +08:00
|
|
|
|
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 })
|
2026-10-07 10:01:05 +08:00
|
|
|
|
return detailRecord(data, id, productId, revisionId).item
|
2026-09-28 14:56:46 +08:00
|
|
|
|
}
|
2026-09-30 19:11:44 +08:00
|
|
|
|
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 })
|
2026-09-28 14:56:46 +08:00
|
|
|
|
const matching = data['matching'] as UTSJSONObject; const status = text(matching, 'status')
|
2026-10-07 10:01:05 +08:00
|
|
|
|
const resolvedRevision = text(data, 'revisionId'); const version = data.getNumber('resultVersion') ?? 0
|
|
|
|
|
|
if (!validRecordId(resolvedRevision) || (revisionId.length > 0 && resolvedRevision != revisionId) || text(data, 'assessmentId') != id || version < 1 || Math.floor(version) != version) throw new Error('匹配结果与本次评估不一致,请重新加载。')
|
|
|
|
|
|
const items = (data['items'] as UTSJSONObject[]).map((v: UTSJSONObject): MatchView => matchView(v))
|
|
|
|
|
|
const hasMore = data.getBoolean('hasMore') ?? false; const nextCursor = text(data, 'nextCursor')
|
|
|
|
|
|
const ids: string[] = []
|
|
|
|
|
|
items.forEach((item: MatchView) => { if (item.state != group || ids.includes(item.id)) throw new Error('匹配分组或产品编号不一致,请重新加载。'); ids.push(item.id) })
|
|
|
|
|
|
if (!['completed','unavailable'].includes(status) || items.length > 3 || (hasMore && (items.length == 0 || nextCursor.length == 0)) || (!hasMore && nextCursor.length > 0) || (status == 'unavailable' && (items.length > 0 || hasMore))) throw new Error('匹配分页状态不完整,请重新加载。')
|
|
|
|
|
|
if (status == 'completed') {
|
|
|
|
|
|
if (text(matching, 'catalogSnapshotId').length == 0 || text(matching, 'ruleVersion').length == 0 || text(matching, 'orderingPolicyVersion').length == 0) throw new Error('匹配快照版本不完整,请重新加载。')
|
|
|
|
|
|
const countKeys = ['candidateCount','ineligibleCount','unknownCount']
|
|
|
|
|
|
countKeys.forEach((key: string) => { const value = matching.getNumber(key); if (value == null || value < 0 || Math.floor(value) != value) throw new Error('匹配统计不完整,请重新加载。') })
|
|
|
|
|
|
}
|
2026-09-28 14:56:46 +08:00
|
|
|
|
const reason = text(matching, 'reasonCode')
|
|
|
|
|
|
const count = (key: string): string => { const v = matching.getNumber(key); return v == null ? '—' : v.toString() }
|
2026-10-14 14:53:13 +08:00
|
|
|
|
const message = status == 'completed' ? (text(matching, 'orderingPolicyVersion') == 'product-score-descending-v1' ? '按本次条件匹配度排序,匹配度不代表获批概率。' : '按本次评估时的产品条件判断,展示顺序不代表优劣。') : (reason == 'LEGACY_INPUT' ? '旧版报告未执行产品匹配,可重新评估获取新的结果。' : (reason == 'MATCH_RULES_UNPUBLISHED' ? '本次评估尚未启用产品匹配规则。' : '本次评估没有可用产品目录,未执行产品匹配。'))
|
2026-10-07 10:01:05 +08:00
|
|
|
|
return { assessmentId: id, revisionId: resolvedRevision, resultVersion: version, snapshotId: text(matching, 'catalogSnapshotId'), ruleVersion: text(matching, 'ruleVersion'), orderingVersion: text(matching, 'orderingPolicyVersion'), status, message, eligible: count('candidateCount'), ineligible: count('ineligibleCount'), unknown: count('unknownCount'), items, hasMore, cursor: nextCursor }
|
2026-09-28 14:56:46 +08:00
|
|
|
|
}
|
2026-09-30 19:11:44 +08:00
|
|
|
|
export async function fetchMatchDetail(id: string, productId: string, revisionId: string = ''): Promise<MatchView> {
|
|
|
|
|
|
return (await fetchMatchDetailRecord(id, productId, revisionId)).item
|
2026-09-28 14:56:46 +08:00
|
|
|
|
}
|
2026-09-30 19:11:44 +08:00
|
|
|
|
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))
|
2026-10-07 10:01:05 +08:00
|
|
|
|
return detailRecord(data, id, productId, revisionId)
|
|
|
|
|
|
}
|
|
|
|
|
|
function detailRecord(data: UTSJSONObject, id: string, productId: string, revisionId: string): MatchDetail {
|
2026-09-28 14:56:46 +08:00
|
|
|
|
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只表达选择意图;读取后再次检查归属和冻结版本,不能用公共资料补齐失败列。
|
2026-10-07 10:01:05 +08:00
|
|
|
|
if (!validRecordId(text(data, 'revisionId')) || (revisionId.length > 0 && text(data, 'revisionId') != revisionId) || text(data, 'assessmentId') != id || item.id != productId || item.product.id != productId || version < 1 || Math.floor(version) != version || snapshotId.length == 0 || text(matching, 'ruleVersion').length == 0 || text(matching, 'orderingPolicyVersion').length == 0 || text(matching, 'status') != 'completed') throw new Error('产品不属于本次冻结匹配结果,请返回重新选择。')
|
2026-09-30 19:11:44 +08:00
|
|
|
|
return { assessmentId: id, revisionId: text(data, 'revisionId'), resultVersion: version, snapshotId, ruleVersion: text(matching, 'ruleVersion'), orderingVersion: text(matching, 'orderingPolicyVersion'), item }
|
2026-09-28 14:56:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
export async function fetchPublicProduct(id: string): Promise<ProductView> {
|
2026-10-07 10:01:05 +08:00
|
|
|
|
const result = productView(await requestJson('/products/' + encodeURIComponent(id), 'GET', new UTSJSONObject()), true)
|
|
|
|
|
|
if (result.id != id || result.version < 1 || Math.floor(result.version) != result.version) throw new Error('产品资料与所选产品不一致,请重新加载。')
|
|
|
|
|
|
return result
|
2026-09-28 14:56:46 +08:00
|
|
|
|
}
|