feat: 支持评估版本隔离的报告、匹配及PDF导出功能
新增revisionId参数到路由、服务及导出接口,实现不同评估版本的隔离访问。删除已废弃的需求相关服务文件,移除取消采集的融资金额、期限和用途字段,更新相关页面提示文本。同时更新文档,说明评估版本、最新资料及产品展示规则的调整,支持前台正常业务名称显示,去除环境标签。
This commit is contained in:
@@ -1,5 +1,9 @@
|
|||||||
# AI 融资移动端
|
# AI 融资移动端
|
||||||
|
|
||||||
|
2026-09-30产品展示更新:正式库现有六条产品已同步正常业务名称和资料,匹配/详情/报告/对比/行动/助手/PDF去除环境标签,内部来源记录保留。微信开发编译及手机/桌面页面联调通过;本轮源码未部署、小程序/H5未发行。备份与恢复、验证及发布边界见[交接](docs/新会话交接.md)顶部和[开发进度0.36](docs/开发进度.md)。
|
||||||
|
|
||||||
|
2026-09-30最新:已实现“最新资料+评估版本”,新建评估新增记录,修改评估回填并在原记录下保存新版本;历史报告/匹配/助手/PDF绑定版本。正式MySQL5.7/airz迁移、配套PHP部署与63项维护验收已完成,服务恢复;小程序/H5尚未发布。微信/Android此前编译通过,体验版API已配置https://aitest.ychysg.cn/api/v1,但外网HTTPS仍连接重置,发行前需解决并复测。完整状态见[交接](docs/新会话交接.md)及[版本契约](docs/评估版本与最新资料.md)。以下日期段落为历史记录;后端.env仍连接远程正式库,本机worker启动说明只可用于显式隔离环境。
|
||||||
|
|
||||||
基于 uni-app x、Vue 3、UTS 的移动端原型,包含 27 个页面。使用 HBuilderX(VDOM 模式)开发,已验证 5.24 和 5.26 的本地编译;本项目没有独立的 npm 构建入口。
|
基于 uni-app x、Vue 3、UTS 的移动端原型,包含 27 个页面。使用 HBuilderX(VDOM 模式)开发,已验证 5.24 和 5.26 的本地编译;本项目没有独立的 npm 构建入口。
|
||||||
|
|
||||||
2026-09-28:原型07产品对比已接通,匹配列表/本次产品详情可选择2—3项历史产品,支持补选、移除、刷新恢复、失败列重试及手机横向滚动。复用本人冻结匹配详情,不新增表或模型调用;当前26页,真实产品与跨平台验收仍待完成。见[开发进度6.16](docs/开发进度.md)。
|
2026-09-28:原型07产品对比已接通,匹配列表/本次产品详情可选择2—3项历史产品,支持补选、移除、刷新恢复、失败列重试及手机横向滚动。复用本人冻结匹配详情,不新增表或模型调用;当前26页,真实产品与跨平台验收仍待完成。见[开发进度6.16](docs/开发进度.md)。
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
# 原型08上下文AI助手
|
# 原型08上下文AI助手
|
||||||
|
|
||||||
|
2026-09-30展示更新:当前产品采用正常业务名称,折叠及展开上下文不显示环境标签。isDemo仍为内部元数据,系统提示词要求不复述该标记,只解释已配置条件和核验清单;冻结、金额精度及不改判规则保持。提示词本机已修改,真实模型尚未复测、线上未部署,历史真实调用结论只适用于当时版本。见[开发进度0.36](开发进度.md)。
|
||||||
|
|
||||||
版本1.3,2026-09-29。CHAT-001—003待验收;CHAT-004开发中。累计13次真实Flash调用,演示边界及金额精度连续三轮专项通过;真机、跨平台、真实产品与商业额度仍待验收。
|
版本1.3,2026-09-29。CHAT-001—003待验收;CHAT-004开发中。累计13次真实Flash调用,演示边界及金额精度连续三轮专项通过;真机、跨平台、真实产品与商业额度仍待验收。
|
||||||
|
|
||||||
## 1. 会话与事实边界
|
## 1. 会话与事实边界
|
||||||
|
|||||||
+45
-3
@@ -1,6 +1,20 @@
|
|||||||
# 产品匹配规则开发契约
|
# 产品匹配规则开发契约
|
||||||
|
|
||||||
版本:1.3。日期:2026-09-24。关联PRODUCT-001、PRODUCT-002、PRODUCT-003。页面消费方式见[04—10方案](原型04-10页面改造与跳转方案.md),完整产品判定/快照/接口语义仍以[数据契约](需求与评估结果数据契约.md)第5节为准。
|
2026-09-30最新展示约定:按用户要求,当前六条产品使用正常业务名称及资料,前台/PDF不展示“演示、非真实”等环境标签;内部source reference、isDemo及containsDemoProducts继续保存,用于追溯既有产品。正式ID22—27已同步文案并增加版本,金融参数/条件不变,历史快照不改写。下文强制显示环境标签的表述仅作阶段记录,本次展示规则优先;部署状态和验证见[开发进度0.36](开发进度.md)。
|
||||||
|
|
||||||
|
## 2026-09-29 最新覆盖约定(inputVersion=3)
|
||||||
|
|
||||||
|
个人/企业信用融资恢复至2026-09-23 19:04需求扩展之前的采集范围:删除融资金额、融资期限、资金用途、其他用途说明;保留经营月数和原有身份/经营收入/资产负债/资料授权。提交使用inputVersion=3,结果仍为resultVersion=2,保留报告三栏、产品匹配、助手、PDF与行动入口。v3不生成需求事实或匹配规则,不把取消采集的字段列为“未提供/待核实”、补填项或完整度扣分项;产品自身额度、期限、利率和用途资料保留。其他真实缺失或来源未核验仍按既有规则处理。
|
||||||
|
|
||||||
|
资质匹配快照标记profileOnly=true并纳入摘要;编译产品规则时移除requestedAmountYuan、requestedTermMonths、purposeCode相关条件。新准备策略self-reported-profile-20260929-v2,个人11项/企业6项,不含需求条件项。旧输入1/2不再接受新建;保留旧报告解析分支仅供归档兼容。
|
||||||
|
|
||||||
|
已按用户授权在111.230.144.231/airz执行:删除报告161/229、关联会话151/219及2条日志;普通助手449及其2条日志保留,会员1个、产品6个保留。fa_user_info删除requested_amount、requested_term_months、purpose_code、purpose_detail四列;input_version、analysis_context属于必要版本和快照元数据,继续保留。两个触发器同步移除需求列校验,输入版本允许1/2/3,接口新建最低3。22张不相关表摘要一致,log_bin_trust_function_creators临时开启后恢复0。备份:D:/AAdesk/202609/airz-backups/restore-profile-20260929-180938/。
|
||||||
|
|
||||||
|
下文v2字段、用途字典、需求覆盖与旧保留历史约定为历史设计,不可据此恢复采集或重跑旧迁移;其他冻结、鉴权、幂等、三态、金额精度要求继续适用。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
版本:1.4。日期:2026-09-30。关联PRODUCT-001、PRODUCT-002、PRODUCT-003。页面消费方式见[04—10方案](原型04-10页面改造与跳转方案.md),完整产品判定/快照/接口语义仍以[数据契约](需求与评估结果数据契约.md)第5节为准。
|
||||||
|
|
||||||
## 1. 本轮实现边界
|
## 1. 本轮实现边界
|
||||||
|
|
||||||
@@ -142,7 +156,7 @@ source为整份产品条件的共同来源:publisher为发布机构(最长10
|
|||||||
|
|
||||||
Product模型保存事务覆盖行锁、预检和写入;编辑合并锁定的最新记录与实际变更字段后验证,避免部分更新漏掉上架状态或使用旧值校验。新增version固定1,更新以最新version加1,不能从表单指定版本。默认后台add/edit/multi均使用模型save;multi任一条失败整批回滚。SQL/Db直写不触发模型事件,未来导入服务必须走模型或同一预检,不得当作发布接口。
|
Product模型保存事务覆盖行锁、预检和写入;编辑合并锁定的最新记录与实际变更字段后验证,避免部分更新漏掉上架状态或使用旧值校验。新增version固定1,更新以最新version加1,不能从表单指定版本。默认后台add/edit/multi均使用模型save;multi任一条失败整批回滚。SQL/Db直写不触发模型事件,未来导入服务必须走模型或同一预检,不得当作发布接口。
|
||||||
|
|
||||||
草稿允许未齐资料和旧`{"version":1,"rules":[]}`。旧不完整上架产品再次编辑/上架会被拒绝,可先下架再补齐;下架不要求满足新增完整性门槛。本轮不批量清理或自动下架现有数据。后台表单仍为原JSON文本框,默认旧示例仅能存草稿;正式上架请按本节格式录入,结构化编辑器与帮助展示后续补充。
|
草稿允许未齐资料和旧`{"version":1,"rules":[]}`。旧不完整上架产品再次编辑/上架会被拒绝,可先下架再补齐;下架不要求满足新增完整性门槛。不批量清理或自动下架现有数据。后台结构化编辑最新实现见第6.5节,原JSON文本框为历史状态。
|
||||||
|
|
||||||
compile(product,dictionary)不读当前时间,返回来源、字典、规范化范围、有效期、地区模式、两类规则及notApplicableByApplicantType。assertPublishable额外按传入日期检查过期和未来核对日期。全国声明由regionMode/regions/source保留,不生成空in;不适用记录包含ruleId和APPLICANT_TYPE_NOT_APPLICABLE,但尚未转换为正式报告证据。
|
compile(product,dictionary)不读当前时间,返回来源、字典、规范化范围、有效期、地区模式、两类规则及notApplicableByApplicantType。assertPublishable额外按传入日期检查过期和未来核对日期。全国声明由regionMode/regions/source保留,不生成空in;不适用记录包含ruleId和APPLICANT_TYPE_NOT_APPLICABLE,但尚未转换为正式报告证据。
|
||||||
|
|
||||||
@@ -154,6 +168,20 @@ compile(product,dictionary)不读当前时间,返回来源、字典、规范
|
|||||||
|
|
||||||
新模型测试及基础回归在事务内生成合成数据后回滚,测试Redis会话清理;产品前后全量记录比较一致。没有真实产品发布、数据库迁移、外部模型或生图调用。未修改模板或前端页面,未将模型路径测试描述为后台浏览器端到端验收。正式worker保持运行;本次脚本没有全库任务恢复扫描或评估任务入队。
|
新模型测试及基础回归在事务内生成合成数据后回滚,测试Redis会话清理;产品前后全量记录比较一致。没有真实产品发布、数据库迁移、外部模型或生图调用。未修改模板或前端页面,未将模型路径测试描述为后台浏览器端到端验收。正式worker保持运行;本次脚本没有全库任务恢复扫描或评估任务入队。
|
||||||
|
|
||||||
|
### 6.5 结构化编辑及真实发布流程(2026-09-30本地完成)
|
||||||
|
|
||||||
|
新增/编辑共用表单:基本参数与有效期、待核实/全国/指定城市、个人/企业各自用途、source四字段及最多44条动态准入条件。新增默认待核实,SQL NULL表示尚未确认;全国必须明确选择并保存[];指定城市必须至少选择一个现有城市代码。NULL地区不是旧规则,但不能上架。补充字段来自既有白名单,数字支持eq/gte/lte/between/in,枚举支持eq/in;金额单位元、不转浮点,in数字集合逗号分隔,枚举多选。经营月数使用独立列,无年龄控件。条件编号保持稳定、全产品唯一、禁止base_前缀;切换字段/运算符保留各自已输入阈值,对象不一致明确提示并拒绝保存。
|
||||||
|
|
||||||
|
不支持的旧JSON、未知字段/用途/地区代码、错误JSON结构或年龄条件标为旧规则,普通保存保留原JSON和年龄。地区JSON文本null不等于SQL NULL,仍视为错误结构。已有来源单独提取,真正旧规则未重建时来源控件可见但禁用。运营明确勾选重建后才替换地区/来源/用途/条件,并清除旧年龄;无自动推断或迁移。来源/用途/条件实质变化递增规则JSON版本,选择顺序变化或单纯地区编辑不增加此版本;产品行版本每次保存仍递增。草稿允许空来源或用途,上架必须完整;空阈值或非法条件不能被静默丢弃。
|
||||||
|
|
||||||
|
POST后台product/preview只读预检当前表单,不修改产品/版本或上架;复用product/add或product/edit权限并检查数据范围。资料检查失败使用FastAdmin code=0,data.issues为[{field,message}],一次收集独立字段缺失/格式/范围/日期/用途及逐条条件问题;可选经营月数空值不列为必填。格式错误的诊断候选只在内存中使用,不能作为保存或上架输入;无问题时仍调用原完整编译器。版本冲突先拒绝预检,不拿旧版本生成通过回执。后台add/edit固定走结构化映射,编辑提交_base_version;服务端事务内锁定最新行核对,冲突拒绝整次写入,不能删除隐藏标记绕过。上架/下架按钮复用product/multi权限和模型,确认后整批事务,任一不完整产品导致全批回滚。无需新增数据库字段或权限节点。
|
||||||
|
|
||||||
|
真实发布顺序:运营提供实际机构材料和责任人 → 核对参数、城市/全国、用途、全部硬条件、有效期与来源索引 → 保存草稿 → 只读发布检查 → 人工核对来源并在获得发布授权后上架 → 用人工金标准验证三态及来源门槛。检查通过只证明格式与完整性,不访问来源地址、不证明真实有效,也不代表银行审批。材料不能包含用户身份资料或私密链接令牌;禁止将演示参数改名当真实产品。
|
||||||
|
|
||||||
|
代码在ProductEditor、admin Product控制器/模型、product/form.html及backend/product.js。本轮326项PHP、89项本机HTTP/浏览器检查通过,桌面/手机尺寸均无新增console/pageerror;正式产品材料、生产部署、真机与业务金标准仍待完成。
|
||||||
|
|
||||||
|
后续待核实与诊断改进:450项PHP、89项HTTP/浏览器、7项清理/完整性检查通过,四条真实公开材料仅存隔离库草稿。具体证据见开发进度0.33节;尚未部署,仍需受理银行确认当前政策和完整硬条件。
|
||||||
|
|
||||||
## 7. 目录冻结、结果持久化与本地演示产品(最新)
|
## 7. 目录冻结、结果持久化与本地演示产品(最新)
|
||||||
|
|
||||||
### 7.1 执行版本与冻结边界
|
### 7.1 执行版本与冻结边界
|
||||||
@@ -232,7 +260,7 @@ MatchResult沿用数据契约第5节:productId、productVersion、eligibility
|
|||||||
|
|
||||||
productSnapshot包含产品ID/版本/名称/机构/类型,amountMinYuan、amountMaxYuan(元的两位小数字符串)、termMinMonths/termMaxMonths(整数月)、annualRateMinPercent/annualRateMaxPercent(百分数字符串)、isDemo、applicantTypes、regions(空数组表示全国)、validFrom/validUntil(日期)、capturedAt、source(publisher/title/reference/checkedOn)、rules、information/condition及创建/更新时间。客户端仅在显示时转万元,例如`"123456.78"`元显示`12.345678 万元`;个人事实金额也按此规则。
|
productSnapshot包含产品ID/版本/名称/机构/类型,amountMinYuan、amountMaxYuan(元的两位小数字符串)、termMinMonths/termMaxMonths(整数月)、annualRateMinPercent/annualRateMaxPercent(百分数字符串)、isDemo、applicantTypes、regions(空数组表示全国)、validFrom/validUntil(日期)、capturedAt、source(publisher/title/reference/checkedOn)、rules、information/condition及创建/更新时间。客户端仅在显示时转万元,例如`"123456.78"`元显示`12.345678 万元`;个人事实金额也按此规则。
|
||||||
|
|
||||||
公共DTO使用当前发布条件rulesByApplicantType按主体分组,不返回eligibility、rank、score、个人理由/事实/证据。repaymentMethod、channel、approvalDuration在两种产品DTO均为null,当前页面显示“未提供”;不得从演示或自由文本猜值。公共详情不等于公共产品列表,GET /products尚未实现。
|
公共DTO使用当前发布条件rulesByApplicantType按主体分组,不返回eligibility、rank、score、个人理由/事实/证据。repaymentMethod、channel、approvalDuration在两种产品DTO均为null,当前页面显示“未提供”;不得从演示或自由文本猜值。公共详情保留;GET /products当前列表实现见8.4节。
|
||||||
|
|
||||||
当前可用性通过一次受限ID集合查询读取,避免逐项查询。下架、删除、未到生效日或过期为unavailable;目录查询异常为unknown;历史快照仍可读。不会把产品当前版本/额度拼回历史详情。AI解释目前not_requested,前端明确提示尚未生成,保留完整确定性规则理由;失败时的提示分支已预留,真实生成与计费/重试待后续任务。
|
当前可用性通过一次受限ID集合查询读取,避免逐项查询。下架、删除、未到生效日或过期为unavailable;目录查询异常为unknown;历史快照仍可读。不会把产品当前版本/额度拼回历史详情。AI解释目前not_requested,前端明确提示尚未生成,保留完整确定性规则理由;失败时的提示分支已预留,真实生成与计费/重试待后续任务。
|
||||||
|
|
||||||
@@ -242,6 +270,20 @@ productSnapshot包含产品ID/版本/名称/机构/类型,amountMinYuan、amou
|
|||||||
|
|
||||||
本轮`tests/match_records.php --worker-stopped`62项验证接口身份/方法、三组、游标分页和篡改、快照归属、未知键投影、损坏数据、下架/过期/删除/改价后历史、旧版报告、重复读取无副作用。产品变化只在测试连接事务内发生并回滚,随机账户及队列清理;停止正式worker以避免恢复扫描拾取合成评估。浏览器结果及截图见开发进度6.13。
|
本轮`tests/match_records.php --worker-stopped`62项验证接口身份/方法、三组、游标分页和篡改、快照归属、未知键投影、损坏数据、下架/过期/删除/改价后历史、旧版报告、重复读取无副作用。产品变化只在测试连接事务内发生并回滚,随机账户及队列清理;停止正式worker以避免恢复扫描拾取合成评估。浏览器结果及截图见开发进度6.13。
|
||||||
|
|
||||||
|
### 8.4 当前公共产品列表(2026-09-30本地完成)
|
||||||
|
|
||||||
|
GET /api/v1/products要求会员Bearer,返回code/msg/time/data,data为items、nextCursor(末页null)与asOfDate(Asia/Shanghai业务日期)。items复用第8.2节公共Product DTO及现有详情投影,无个人事实/三态/评分/排名。不新增小程序列表页面,本阶段用于目录服务和后续页面接入。
|
||||||
|
|
||||||
|
| 参数 | 规则 |
|
||||||
|
| --- | --- |
|
||||||
|
| type | 可空;personal或enterprise,按产品适用对象过滤 |
|
||||||
|
| cityCode | 可空;现有5城市代码,匹配全国或明确含该城市的产品 |
|
||||||
|
| q | 可空;产品名或机构名,最长100字,拒绝控制字符;%和_按字面查询 |
|
||||||
|
| limit | 默认20;1—50的字符串整数 |
|
||||||
|
| cursor | 服务端返回的不透明游标,长度不超过2048;不能自行拼接 |
|
||||||
|
|
||||||
|
ID升序keyset分页,一次查询整页及下一项,不逐项查询详情。只读当前有效上架产品;草稿、下架、未生效或过期不返回。列表不是冻结历史,分页期间目录可能变化,从首屏重读才能获取最新集合。游标使用既有AES-GCM,绑定会员、全部筛选(包括limit)及业务日期;跨账号、换条件、跨天或篡改返回400 PRODUCT_CURSOR_INVALID。参数不合法422 VALIDATION_ERROR、未登录401、命中的发布资料损坏409 PRODUCT_CONFIGURATION_INVALID;不将损坏目录伪装为零产品。所有响应no-store,既有GET /products/:id DTO保持一致。
|
||||||
|
|
||||||
## 9. 单产品解释任务与原型04/05(2026-09-28已实现,覆盖第7/8节的未接入状态)
|
## 9. 单产品解释任务与原型04/05(2026-09-28已实现,覆盖第7/8节的未接入状态)
|
||||||
|
|
||||||
### 9.1 接口与状态
|
### 9.1 接口与状态
|
||||||
|
|||||||
@@ -0,0 +1,230 @@
|
|||||||
|
{
|
||||||
|
"reportVersion": "product-material-review-v1",
|
||||||
|
"batchId": "public-bank-materials-20260930-v1",
|
||||||
|
"asOfDate": "2026-09-30",
|
||||||
|
"materialsSha256": "c5bc2a6aa15e827d7ba5746ea3ff3ff8125f63520ae6ccfd309f88110e04208d",
|
||||||
|
"environment": "memory_only",
|
||||||
|
"assessmentPolicy": {
|
||||||
|
"inputVersion": 3,
|
||||||
|
"profileOnly": true,
|
||||||
|
"excludedMatchingFields": [
|
||||||
|
"requestedAmountYuan",
|
||||||
|
"requestedTermMonths",
|
||||||
|
"purposeCode"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"summary": {
|
||||||
|
"productCount": 4,
|
||||||
|
"formatCheckPassedCount": 0,
|
||||||
|
"bankVerifiedCount": 0,
|
||||||
|
"publishableCount": 0
|
||||||
|
},
|
||||||
|
"products": [
|
||||||
|
{
|
||||||
|
"materialId": "ICBC-REJ",
|
||||||
|
"productName": "工银融e借",
|
||||||
|
"regionMode": "unverified",
|
||||||
|
"source": {
|
||||||
|
"publisher": "中国工商银行",
|
||||||
|
"title": "个人信用贷款(融e借):业务简介、申请办理、贷款利率",
|
||||||
|
"reference": "https://www.icbc.com.cn/icbc/个人金融/贷款/消费贷款/个人信用消费贷款.htm",
|
||||||
|
"checkedOn": "2026-09-30"
|
||||||
|
},
|
||||||
|
"formatCheckPassed": false,
|
||||||
|
"publicationIssues": [
|
||||||
|
{
|
||||||
|
"field": "amount_min",
|
||||||
|
"message": "最低额度(元)未填写"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"field": "annual_rate_min",
|
||||||
|
"message": "最低年化利率(%)未填写"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"field": "annual_rate_max",
|
||||||
|
"message": "最高年化利率(%)未填写"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"field": "term_min_months",
|
||||||
|
"message": "最短期限(月)未填写"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"field": "valid_from",
|
||||||
|
"message": "生效日期未填写"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"field": "valid_until",
|
||||||
|
"message": "失效日期未填写"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"field": "regions",
|
||||||
|
"message": "适用地区待核实,请确认全国或指定城市"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"field": "purposes.personal",
|
||||||
|
"message": "个人适用用途未填写"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"bankQuestions": [
|
||||||
|
"最低额度",
|
||||||
|
"最短期限",
|
||||||
|
"可用年化利率范围及浮动机制",
|
||||||
|
"适用地区",
|
||||||
|
"有效期",
|
||||||
|
"可执行且完整的准入条件",
|
||||||
|
"细分消费用途映射"
|
||||||
|
],
|
||||||
|
"bankVerification": "pending",
|
||||||
|
"mayPublish": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"materialId": "CMB-SDD-ONLINE",
|
||||||
|
"productName": "闪电贷(线上公开材料)",
|
||||||
|
"regionMode": "unverified",
|
||||||
|
"source": {
|
||||||
|
"publisher": "招商银行南京分行(转载现代快报)",
|
||||||
|
"title": "“闪电贷”赋能美好生活,普惠金融触手可及",
|
||||||
|
"reference": "https://branch.cmbchina.com/0025/branchnews/nj2025052301.htm",
|
||||||
|
"checkedOn": "2026-09-30"
|
||||||
|
},
|
||||||
|
"formatCheckPassed": false,
|
||||||
|
"publicationIssues": [
|
||||||
|
{
|
||||||
|
"field": "amount_min",
|
||||||
|
"message": "最低额度(元)未填写"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"field": "annual_rate_min",
|
||||||
|
"message": "最低年化利率(%)未填写"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"field": "annual_rate_max",
|
||||||
|
"message": "最高年化利率(%)未填写"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"field": "term_min_months",
|
||||||
|
"message": "最短期限(月)未填写"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"field": "valid_from",
|
||||||
|
"message": "生效日期未填写"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"field": "valid_until",
|
||||||
|
"message": "失效日期未填写"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"field": "regions",
|
||||||
|
"message": "适用地区待核实,请确认全国或指定城市"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"bankQuestions": [
|
||||||
|
"最低额度",
|
||||||
|
"最短期限",
|
||||||
|
"年化利率范围",
|
||||||
|
"南京地区实际受理范围及当前政策",
|
||||||
|
"有效期",
|
||||||
|
"完整准入条件",
|
||||||
|
"医疗、购车、旅游与现有用途字典的准确映射"
|
||||||
|
],
|
||||||
|
"bankVerification": "pending",
|
||||||
|
"mayPublish": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"materialId": "ABC-WJD",
|
||||||
|
"productName": "农银微捷贷",
|
||||||
|
"regionMode": "nationwide",
|
||||||
|
"source": {
|
||||||
|
"publisher": "中国农业银行",
|
||||||
|
"title": "微捷贷官方产品介绍及产品指南(官网长图)",
|
||||||
|
"reference": "https://www.abchina.com/cn/sme/National/wjd/202207/t20220721_2168788.htm",
|
||||||
|
"checkedOn": "2026-09-30"
|
||||||
|
},
|
||||||
|
"formatCheckPassed": false,
|
||||||
|
"publicationIssues": [
|
||||||
|
{
|
||||||
|
"field": "amount_min",
|
||||||
|
"message": "最低额度(元)未填写"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"field": "annual_rate_max",
|
||||||
|
"message": "最高年化利率(%)未填写"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"field": "term_min_months",
|
||||||
|
"message": "最短期限(月)未填写"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"field": "valid_from",
|
||||||
|
"message": "生效日期未填写"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"field": "valid_until",
|
||||||
|
"message": "失效日期未填写"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"bankQuestions": [
|
||||||
|
"最低额度",
|
||||||
|
"最短期限",
|
||||||
|
"当前适用的年化利率上限及3.5%起是否仍有效",
|
||||||
|
"有效期",
|
||||||
|
"具体分行受理和完整准入条件"
|
||||||
|
],
|
||||||
|
"bankVerification": "pending",
|
||||||
|
"mayPublish": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"materialId": "CCB-SYD",
|
||||||
|
"productName": "税易贷",
|
||||||
|
"regionMode": "unverified",
|
||||||
|
"source": {
|
||||||
|
"publisher": "中国建设银行",
|
||||||
|
"title": "小微企业产品服务:税易贷",
|
||||||
|
"reference": "https://www2.ccb.com/chn/smallcompany/server/xyd/syd/index.shtml",
|
||||||
|
"checkedOn": "2026-09-30"
|
||||||
|
},
|
||||||
|
"formatCheckPassed": false,
|
||||||
|
"publicationIssues": [
|
||||||
|
{
|
||||||
|
"field": "amount_min",
|
||||||
|
"message": "最低额度(元)未填写"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"field": "annual_rate_min",
|
||||||
|
"message": "最低年化利率(%)未填写"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"field": "annual_rate_max",
|
||||||
|
"message": "最高年化利率(%)未填写"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"field": "term_min_months",
|
||||||
|
"message": "最短期限(月)未填写"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"field": "valid_from",
|
||||||
|
"message": "生效日期未填写"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"field": "valid_until",
|
||||||
|
"message": "失效日期未填写"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"field": "regions",
|
||||||
|
"message": "适用地区待核实,请确认全国或指定城市"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"bankQuestions": [
|
||||||
|
"最低额度",
|
||||||
|
"最短期限",
|
||||||
|
"年化利率范围",
|
||||||
|
"适用地区",
|
||||||
|
"有效期",
|
||||||
|
"纳税和经营准入门槛",
|
||||||
|
"企业主及配偶连带保证的办理细则"
|
||||||
|
],
|
||||||
|
"bankVerification": "pending",
|
||||||
|
"mayPublish": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
# 首批银行产品录入材料
|
||||||
|
|
||||||
|
查阅日期:2026-09-30。用途:本机产品后台草稿。对应结构化文件:[首批产品草稿](首批产品草稿-20260930.json)。
|
||||||
|
|
||||||
|
这批是依据银行公开页面整理的真实产品资料草稿,不是演示产品,也不是银行已核准的当前政策。只录入有出处的参数,尚未确认的参数保留空值;全部保持草稿,不用于会员目录和实际匹配。页面可访问和来源核对日期不能代替银行对有效期、地区及当前利率的确认。
|
||||||
|
|
||||||
|
## 1. 录入概览
|
||||||
|
|
||||||
|
| 产品 | 适用对象 | 公开最高额度 | 公开最长期限 | 年化利率 | 地区处理 |
|
||||||
|
| --- | --- | --- | --- | --- | --- |
|
||||||
|
| 工银融e借 | 个人 | 500万元 | 84个月 | LPR加成浮动,未公开范围 | NULL,待当地分行确认 |
|
||||||
|
| 闪电贷(线上公开材料) | 个人 | 30万元 | 84个月 | 该材料未公开 | NULL,来源为南京分行,当前城市字典不含南京 |
|
||||||
|
| 农银微捷贷 | 小微企业 | 300万元 | 12个月 | 官网长图3.5%起,上限未知 | [],官网明确列为全国通用产品,仍需确认受理分行 |
|
||||||
|
| 税易贷 | 小微企业 | 300万元 | 12个月 | 官网未公开 | NULL,待受理分行确认 |
|
||||||
|
|
||||||
|
后台金额输入单位为元,分别填 `5000000.00`、`300000.00`、`3000000.00`、`3000000.00`。不要把万元数字直接写入金额列。`3.5000`表示3.5%,不是0.035;“起”不能填成固定利率或上下限相同。
|
||||||
|
|
||||||
|
四条的最低额度、最短期限、有效期均未从本次来源确认,保持NULL。官网未披露具体经营月数、收入、负债、查询次数等门槛,补充准入规则均为空,不编造“经营满两年”“纳税A/B级”“月收入5000元”之类条件。
|
||||||
|
|
||||||
|
## 2. 工银融e借
|
||||||
|
|
||||||
|
来源:中国工商银行官网[个人信用贷款(融e借)](https://www.icbc.com.cn/icbc/个人金融/贷款/消费贷款/个人信用消费贷款.htm)。分别查看“业务简介”“申请办理”“贷款利率”三个栏目。
|
||||||
|
|
||||||
|
可录入的原文依据:
|
||||||
|
|
||||||
|
- “贷款额度:最高可借500万”“贷款期限:最长可达7年”。这是官网产品上限,不等于普通线上申请人都能获得该额度。
|
||||||
|
- 年满18周岁的具有完全民事行为能力的中国公民;65周岁以上主要适用于一级直属分行认定的优质客户。
|
||||||
|
- “拥有稳定的收入来源、可变现的资产或可靠的还款保障。”这是替代关系,不能简化成每人都须达到某个固定月收入。
|
||||||
|
- 利率为按最新一年LPR加成的年化浮动利率,按月更新;未给可录入的上下限。
|
||||||
|
- 提供等额本息、等额本金、按期付息一次性还本等方式。
|
||||||
|
|
||||||
|
禁止用途保留在“不适用情况”:购房及偿还住房抵押贷款;股票、债券、期货、金融衍生品、资管产品等投资;固定资产或股本权益投资;其他违法用途。
|
||||||
|
|
||||||
|
官网提示具体办理标准以当地分行为准,因此不直接选择“全国”。本次来源未明确对应系统的细分消费用途,个人用途数组先留空。系统当前不采集年龄,也不能表达银行的优质客户例外,这些内容仅存准入说明,不能用身份证自行计算年龄来增加硬规则。
|
||||||
|
|
||||||
|
准备建议:本人证件、账户、收入或资产及真实用途材料。此清单是整理建议,官网未给统一必备材料清单。
|
||||||
|
|
||||||
|
## 3. 闪电贷(线上公开材料)
|
||||||
|
|
||||||
|
来源:招商银行南京分行官网[“闪电贷”赋能美好生活,普惠金融触手可及](https://branch.cmbchina.com/0025/branchnews/nj2025052301.htm),注明转自现代快报。网址含20250523,正文未见独立发布日期;不能把搜索结果的抓取日期当作发布日期,也不能认定政策在2026年仍有效。
|
||||||
|
|
||||||
|
原文区分两种渠道:线上最高30万元;线下补充公积金、房产证明等材料可提额至50万元。本批只录入线上30万元,不能把50万元与线上产品参数合并。
|
||||||
|
|
||||||
|
原文列明贷款期限由5年延至7年,支持装修、教育、医疗、购车、旅游等消费需求。当前用途字典直接对应的仅选“家居装修”“教育进修”;其余用途保留在说明,未把“其他用途”当作无限放开。
|
||||||
|
|
||||||
|
这是南京分行公开报道。当前系统仅支持深圳、广州、北京、上海、杭州五城市,不能将南京替换成其他城市或全国。地区选择“待核实”并用NULL保存,待取得受理范围和系统支持的地区代码后再明确选择;本轮不扩充城市字典。
|
||||||
|
|
||||||
|
完整准入规则、利率、最低额度、最短期限和有效期未取得。公积金、房产证明是报道中的线下提额材料,不能将其设成所有线上客户的必选条件。
|
||||||
|
|
||||||
|
准备建议:本人证件、招行账户、真实消费用途材料;线下提额材料另向银行确认。
|
||||||
|
|
||||||
|
## 4. 农银微捷贷
|
||||||
|
|
||||||
|
来源:农业银行[全国通用产品微捷贷栏目](https://www.abchina.com/cn/sme/National/wjd/)及[官方产品长图与指南](https://www.abchina.com/cn/sme/National/wjd/202207/t20220721_2168788.htm)。参数和条件位于长图,需要查看图片,不能仅凭搜索摘要录入。
|
||||||
|
|
||||||
|
原文可确认:
|
||||||
|
|
||||||
|
- 小微企业流动资金用途,额度最高300万元,期限最长12个月。
|
||||||
|
- 小微企业与法定代表人为共同借款人。
|
||||||
|
- 企业依法设立、经营和信用状况良好;法定代表人为中国内地居民,不含港澳台;以及银行要求的其他条件。
|
||||||
|
- 信用方式用信,无需抵质押担保。
|
||||||
|
- “年化3.5%起,单利计算,具体以实际审批为准。”只记录公开下限,利率上限留空;详情网址较早,当前执行利率必须重核。
|
||||||
|
- 按月或季还息、一次还本或一次性利随本清,具体以合同为准。
|
||||||
|
- 官方明确不与任何第三方合作发放本贷款,不收取合同约定外费用。
|
||||||
|
|
||||||
|
用途只选企业“经营周转”。全国通用栏目支持使用 `regions=[]`,不意味着全国每个网点都受理。未取得纳税评级、经营月数等数字准入阈值,不能采用第三方营销文章补造;“纳税e贷”额度试算页面也不作为另一个独立产品重复录入。
|
||||||
|
|
||||||
|
官网指南提及企业和法定代表人在银行渠道完成业务、征信及涉税授权。系统只提供材料准备,本次没有接入、代签或提交这些银行授权。
|
||||||
|
|
||||||
|
准备建议:企业证照、法定代表人身份、企业账户及经营、涉税资料。实际材料及共同借款细节由银行确认。
|
||||||
|
|
||||||
|
## 5. 税易贷
|
||||||
|
|
||||||
|
来源:建设银行官网[小微企业产品服务:税易贷](https://www2.ccb.com/chn/smallcompany/server/xyd/syd/index.shtml)。本条产品名为“税易贷”,不混用“云税贷”的参数。
|
||||||
|
|
||||||
|
原文说明为对按时足额纳税的优质纳税小微企业发放、用于短期生产经营周转的可循环人民币信用贷款,额度最高300万元、期限最长1年。
|
||||||
|
|
||||||
|
必须保留的担保条款:“企业主及配偶提供连带责任保证。”虽然称为信用贷款,仍有保证要求,不能写成无担保。
|
||||||
|
|
||||||
|
页面未提供统一利率、具体纳税额或评级门槛、经营年限和有效期。这些内容均不补写成规则。网页顶部“重庆市”是站点地区选择,正文没有据此限定产品受理地区,因此既不设重庆也不默认全国。
|
||||||
|
|
||||||
|
用途选企业“经营周转”。准备建议为企业证照、经营和纳税资料,以及企业主、配偶身份和关系及保证相关资料;实际清单向受理分行确认。
|
||||||
|
|
||||||
|
## 6. 保存和上架边界
|
||||||
|
|
||||||
|
结构化文件中的 `draft` 是草稿字段;`quotes` 是本次查阅的关键原文,`missingBeforePublish` 是银行待确认事项,`suggestedPreparation` 是整理建议。三者不能互相替代。
|
||||||
|
|
||||||
|
后续本机开发已新增“待核实”地区选项,新增产品也默认待核实。三条NULL地区草稿不再显示旧规则提示,可正常编辑并查看已保存的来源;普通保存继续保留SQL NULL。全国必须明确选择,不能用[]代替未知地区;农银微捷贷保留已有全国通用依据。真正旧规则的来源仍可只读查看,未明确重建时普通保存保留原JSON。
|
||||||
|
|
||||||
|
发布前检查会一次列出独立字段缺失或错误,包括地区待核实、额度/利率/期限、有效期、来源、用途和逐条条件问题;检查只读,不改变草稿或规则版本。未知地区仍不能上架,格式检查通过也不能代替受理银行核实政策。
|
||||||
|
|
||||||
|
四条均缺少发布所需参数。发布前至少取得受理机构对地区、当前参数、有效期及完整准入条件的确认,再检查系统能否表达全部硬条件。保证、共同借款主体等重要条件目前不能由采集字段完整核验,不能仅因基础参数完整就将产品投入正式匹配。
|
||||||
|
|
||||||
|
本次未变更资料采集字段。用户仍无需填写已取消的融资金额、期限和用途;产品本身额度、期限、用途资料继续保留。没有修改已有演示产品、历史报告或正式数据库。
|
||||||
|
|
||||||
|
## 7. 本机录入结果
|
||||||
|
|
||||||
|
四条已保存到 `127.0.0.1:3309/airz_product_test_20260930`,ID依次为1、2、3、4,全部 `switch=0`。本机后台:[产品列表](http://127.0.0.1:8097/ZYurKboadX.php/product/index)。账号使用现有本机测试管理员,凭据不写入本材料。
|
||||||
|
|
||||||
|
已核对金额元字符串、期限月数、NULL参数、利率单边下限、来源及地区保存;四条均无法通过发布检查。重复执行仅核对已有内容,未重复插入;会员、最新资料、会话、日志及用量表计数未变。后台列表显示四条草稿,微捷贷结构化编辑页显示来源和空值。本机服务使用原隔离配置、模型关闭,没有启动分析worker。
|
||||||
|
|
||||||
|
材料录入当轮未修改源码;后续编辑器改进见开发进度0.33节,未改写本批四条草稿。临时录入脚本 `C:/Users/JUN/AppData/Local/Temp/airz-product-editor-20260930/import-public-materials.php` 在读框架前验证固定本机库,只允许草稿,在事务中保存,并拒绝覆盖后续人工改动。无凭据回执为同目录 `public-materials-import.json`,来源与列表截图亦在该目录。临时文件可能被系统清理,需恢复材料时以本目录JSON和原文核对为准,不运行旧清理脚本删除本批产品。
|
||||||
@@ -0,0 +1,207 @@
|
|||||||
|
{
|
||||||
|
"batchId": "public-bank-materials-20260930-v1",
|
||||||
|
"checkedOn": "2026-09-30",
|
||||||
|
"intendedEnvironment": "local_draft_only",
|
||||||
|
"notes": [
|
||||||
|
"依据可访问的官方公开页面整理;核对日期只表示查阅页面,不表示银行确认当前政策或批准上架。",
|
||||||
|
"金额为元字符串,期限为月,利率为年化百分比。未知参数使用null,不能补成0或自行推定有效期。",
|
||||||
|
"regions=null表示地区尚不能确定;regions=[]仅用于官网明确列为全国通用的产品。",
|
||||||
|
"conditions为空表示未取得能由现有字段准确表达的完整准入规则,不表示银行没有准入要求。",
|
||||||
|
"仅作为本机草稿保存,不能据此判定申请人符合银行全部条件或作出授信承诺。"
|
||||||
|
],
|
||||||
|
"products": [
|
||||||
|
{
|
||||||
|
"materialId": "ICBC-REJ",
|
||||||
|
"sourceScope": "工商银行总行官网产品说明;具体办理标准仍以当地分行规定为准。",
|
||||||
|
"quotes": [
|
||||||
|
{"topic": "额度", "text": "贷款额度:最高可借500万;"},
|
||||||
|
{"topic": "期限", "text": "贷款期限:最长可达7年;"},
|
||||||
|
{"topic": "利率", "text": "实行浮动利率(年化),根据最新一年贷款市场报价利率(LPR)加成计算,按月更新。"},
|
||||||
|
{"topic": "还款保障", "text": "拥有稳定的收入来源、可变现的资产或可靠的还款保障。"},
|
||||||
|
{"topic": "地区边界", "text": "本页面内容仅供参考,具体业务办理标准以中国工商银行当地分行规定为准。"}
|
||||||
|
],
|
||||||
|
"missingBeforePublish": ["最低额度", "最短期限", "可用年化利率范围及浮动机制", "适用地区", "有效期", "可执行且完整的准入条件", "细分消费用途映射"],
|
||||||
|
"suggestedPreparation": ["本人有效身份证件及银行账户", "收入、资产或其他还款保障材料", "真实资金用途材料;具体清单由银行确认"],
|
||||||
|
"draft": {
|
||||||
|
"product_name": "工银融e借",
|
||||||
|
"product_type": "个人信用贷款",
|
||||||
|
"bank_name": "中国工商银行",
|
||||||
|
"object": "1",
|
||||||
|
"switch": 0,
|
||||||
|
"amount_min": null,
|
||||||
|
"amount_max": "5000000.00",
|
||||||
|
"term_min_months": null,
|
||||||
|
"term_max_months": 84,
|
||||||
|
"annual_rate_min": null,
|
||||||
|
"annual_rate_max": null,
|
||||||
|
"age_min": null,
|
||||||
|
"age_max": null,
|
||||||
|
"operating_months_min": null,
|
||||||
|
"regions": null,
|
||||||
|
"valid_from": null,
|
||||||
|
"valid_until": null,
|
||||||
|
"condition": "官网要求:年满18周岁的具有完全民事行为能力的中国公民;65周岁以上主要适用于一级直属分行认定的优质客户;具有稳定收入、可变现资产或可靠还款保障。年龄和还款保障的替代关系无法由现有字段完整表达,仅保留原意说明。",
|
||||||
|
"information": "官网未给统一材料清单。准备建议:本人有效身份证件、银行账户、收入或资产及用途材料;实际要求向当地工商银行确认,不作为银行必备清单。",
|
||||||
|
"recommendation": "官网列明无需担保、无需抵押;支持线上及网点办理,提供等额本息、等额本金、按期付息一次性还本等方式。额度和期限以审批为准。",
|
||||||
|
"deprecated": "资金不得用于购房及偿还住房抵押贷款;股票、债券、期货、金融衍生品和资管产品等投资;固定资产、股本权益性投资;法律法规禁止的其他用途。",
|
||||||
|
"grade": "官网未公开可执行评分规则,不生成匹配分数或授信成功率。",
|
||||||
|
"other": "公开材料草稿批次public-bank-materials-20260930-v1;查阅日期2026-09-30。来源:工商银行官网个人信用贷款(融e借)页面。网址:https://www.icbc.com.cn/icbc/个人金融/贷款/消费贷款/个人信用消费贷款.htm。最高500万元、最长7年是官网产品上限,不代表普通线上客户一定可获得。浮动利率未提供可填的上下限。地区、最低额度、最短期限、有效期和完整准入条件待银行核实,草稿不得上架。",
|
||||||
|
"eligibility_rules": {
|
||||||
|
"version": 1,
|
||||||
|
"source": {
|
||||||
|
"publisher": "中国工商银行",
|
||||||
|
"title": "个人信用贷款(融e借):业务简介、申请办理、贷款利率",
|
||||||
|
"reference": "https://www.icbc.com.cn/icbc/个人金融/贷款/消费贷款/个人信用消费贷款.htm",
|
||||||
|
"checkedOn": "2026-09-30"
|
||||||
|
},
|
||||||
|
"purposes": {"personal": []},
|
||||||
|
"conditions": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"materialId": "CMB-SDD-ONLINE",
|
||||||
|
"sourceScope": "招商银行南京分行官网转载现代快报的消费金融报道;网址包含20250523,正文未见独立发布日期。属于分行公开材料,不能直接外推全国或认定2026年政策仍有效。",
|
||||||
|
"quotes": [
|
||||||
|
{"topic": "渠道额度", "text": "招行“闪电贷”个人线上最高授信额度达30万元,线下通过补充公积金、房产证明等材料可提额至50万元"},
|
||||||
|
{"topic": "期限", "text": "贷款期限由5年延至7年,客户还可选择灵活分期方案"},
|
||||||
|
{"topic": "用途", "text": "满足装修、教育、医疗、购车、旅游等消费需求"}
|
||||||
|
],
|
||||||
|
"missingBeforePublish": ["最低额度", "最短期限", "年化利率范围", "南京地区实际受理范围及当前政策", "有效期", "完整准入条件", "医疗、购车、旅游与现有用途字典的准确映射"],
|
||||||
|
"suggestedPreparation": ["本人有效身份证件和招行账户", "实际消费用途材料", "仅在线下提额时向银行确认公积金或房产证明要求"],
|
||||||
|
"draft": {
|
||||||
|
"product_name": "闪电贷(线上公开材料)",
|
||||||
|
"product_type": "个人消费贷款",
|
||||||
|
"bank_name": "招商银行",
|
||||||
|
"object": "1",
|
||||||
|
"switch": 0,
|
||||||
|
"amount_min": null,
|
||||||
|
"amount_max": "300000.00",
|
||||||
|
"term_min_months": null,
|
||||||
|
"term_max_months": 84,
|
||||||
|
"annual_rate_min": null,
|
||||||
|
"annual_rate_max": null,
|
||||||
|
"age_min": null,
|
||||||
|
"age_max": null,
|
||||||
|
"operating_months_min": null,
|
||||||
|
"regions": null,
|
||||||
|
"valid_from": null,
|
||||||
|
"valid_until": null,
|
||||||
|
"condition": "分行官网报道说明线上消费授信和线下补充材料提额的区别,未公开完整准入要求。不能将公积金、房产或某个收入门槛设置为所有线上申请人的必需条件。",
|
||||||
|
"information": "官网报道仅提及线下提额可补充公积金、房产证明。一般准备建议:本人证件、招行账户、真实消费用途材料;具体材料由银行确认。",
|
||||||
|
"recommendation": "分行公开材料列明支持装修、教育、医疗、购车、旅游等消费需求,期限最长7年;线上最高30万元,实际以当前审批为准。",
|
||||||
|
"deprecated": "本条仅整理线上材料,线下提额最高50万元未混入线上额度。不能据该分行报道宣称全国同政策或将消费贷款用于经营、投资等未获银行确认的用途。",
|
||||||
|
"grade": "未取得银行评分规则,不设置固定分数、通过率或负债率门槛。",
|
||||||
|
"other": "公开材料草稿批次public-bank-materials-20260930-v1;查阅日期2026-09-30。来源:招商银行南京分行《“闪电贷”赋能美好生活,普惠金融触手可及》,转自现代快报。网址:https://branch.cmbchina.com/0025/branchnews/nj2025052301.htm。南京不在当前五城市字典内,地区保留NULL,不能设为全国或替换成上海。最低额度、最短期限、利率、有效期与完整准入条件待核实;本批只映射明确的家居装修和教育进修用途。",
|
||||||
|
"eligibility_rules": {
|
||||||
|
"version": 1,
|
||||||
|
"source": {
|
||||||
|
"publisher": "招商银行南京分行(转载现代快报)",
|
||||||
|
"title": "“闪电贷”赋能美好生活,普惠金融触手可及",
|
||||||
|
"reference": "https://branch.cmbchina.com/0025/branchnews/nj2025052301.htm",
|
||||||
|
"checkedOn": "2026-09-30"
|
||||||
|
},
|
||||||
|
"purposes": {"personal": ["education", "home_improvement"]},
|
||||||
|
"conditions": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"materialId": "ABC-WJD",
|
||||||
|
"sourceScope": "农业银行官网全国通用产品栏目及微捷贷官方长图;详情网址含20220721,当前可访问不等于银行确认2026年所有参数仍有效。",
|
||||||
|
"quotes": [
|
||||||
|
{"topic": "额度及期限", "text": "满足小微企业生产经营过程中的流动资金需求,贷款额度最高300万元,贷款期限最长12个月。"},
|
||||||
|
{"topic": "借款主体", "text": "小微企业及其法定代表人为贷款的共同借款人。"},
|
||||||
|
{"topic": "担保方式", "text": "信用方式用信,无需提供抵(质)押担保。"},
|
||||||
|
{"topic": "利率", "text": "年化3.5%起,单利计算,具体以实际审批为准。"},
|
||||||
|
{"topic": "第三方边界", "text": "农业银行不会与任何第三方合作发放“农银微捷贷”贷款,不会向借款人收取合同约定之外的任何费用。"}
|
||||||
|
],
|
||||||
|
"missingBeforePublish": ["最低额度", "最短期限", "当前适用的年化利率上限及3.5%起是否仍有效", "有效期", "具体分行受理和完整准入条件"],
|
||||||
|
"suggestedPreparation": ["企业证照、法定代表人身份及企业账户资料", "工商、涉税和征信资料", "经营流水及资金用途说明;以银行要求为准"],
|
||||||
|
"draft": {
|
||||||
|
"product_name": "农银微捷贷",
|
||||||
|
"product_type": "小微企业信用贷款",
|
||||||
|
"bank_name": "中国农业银行",
|
||||||
|
"object": "2",
|
||||||
|
"switch": 0,
|
||||||
|
"amount_min": null,
|
||||||
|
"amount_max": "3000000.00",
|
||||||
|
"term_min_months": null,
|
||||||
|
"term_max_months": 12,
|
||||||
|
"annual_rate_min": "3.5000",
|
||||||
|
"annual_rate_max": null,
|
||||||
|
"age_min": null,
|
||||||
|
"age_max": null,
|
||||||
|
"operating_months_min": null,
|
||||||
|
"regions": [],
|
||||||
|
"valid_from": null,
|
||||||
|
"valid_until": null,
|
||||||
|
"condition": "官方长图要求:企业依法设立、经营情况良好;企业信用状况良好;企业法定代表人为中国内地居民(不含港澳台);满足农行其他办理条件。企业与法定代表人为共同借款人。未公开具体经营月数、纳税评级或收入门槛,不根据第三方营销材料补写规则。",
|
||||||
|
"information": "官网产品指南列明企业和法定代表人在银行渠道进行业务、征信及涉税信息授权。准备建议:企业证照、法定代表人身份、账户、经营及涉税资料;不是银行统一必备清单,本系统不代银行获取授权或提交申请。",
|
||||||
|
"recommendation": "官方资料说明在线自助循环信用贷款,无需抵质押担保;支持流动资金周转。额度最高300万元、最长期限12个月。按月或季还息、一次还本或一次性利随本清,以合同为准。",
|
||||||
|
"deprecated": "未经核实不能宣称纳税A级或B级、经营满24个月即可通过,也不能把纳税e贷与本产品当作两个独立产品重复录入。官方明确不与任何第三方合作发放本贷款。",
|
||||||
|
"grade": "未公开可执行评分策略,不给出授信成功率。",
|
||||||
|
"other": "公开材料草稿批次public-bank-materials-20260930-v1;查阅日期2026-09-30。来源:https://www.abchina.com/cn/sme/National/wjd/202207/t20220721_2168788.htm,官方长图;全国通用栏目:https://www.abchina.com/cn/sme/National/wjd/。全国通用不代表每个网点都受理,分行政策仍需确认。3.5%起仅填利率下限,不把上限也填成3.5%。当前利率、最低额度、最短期限、有效期和完整准入条件未获银行确认,保持草稿。",
|
||||||
|
"eligibility_rules": {
|
||||||
|
"version": 1,
|
||||||
|
"source": {
|
||||||
|
"publisher": "中国农业银行",
|
||||||
|
"title": "微捷贷官方产品介绍及产品指南(官网长图)",
|
||||||
|
"reference": "https://www.abchina.com/cn/sme/National/wjd/202207/t20220721_2168788.htm",
|
||||||
|
"checkedOn": "2026-09-30"
|
||||||
|
},
|
||||||
|
"purposes": {"enterprise": ["working_capital"]},
|
||||||
|
"conditions": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"materialId": "CCB-SYD",
|
||||||
|
"sourceScope": "建设银行总行官网税易贷产品页,不与云税贷、税易贷其他分行版本混用。",
|
||||||
|
"quotes": [
|
||||||
|
{"topic": "产品及用途", "text": "建设银行对按时足额纳税的小微企业发放的,用于短期生产经营周转的可循环的人民币信用贷款业务。"},
|
||||||
|
{"topic": "额度", "text": "贷款额度最高不超过人民币300万元。"},
|
||||||
|
{"topic": "期限", "text": "最长不超过1年。"},
|
||||||
|
{"topic": "担保条件", "text": "企业主及配偶提供连带责任保证。"}
|
||||||
|
],
|
||||||
|
"missingBeforePublish": ["最低额度", "最短期限", "年化利率范围", "适用地区", "有效期", "纳税和经营准入门槛", "企业主及配偶连带保证的办理细则"],
|
||||||
|
"suggestedPreparation": ["企业证照及经营资料", "企业纳税资料", "企业主及配偶身份、关系和保证相关资料;具体由银行确认"],
|
||||||
|
"draft": {
|
||||||
|
"product_name": "税易贷",
|
||||||
|
"product_type": "小微企业纳税信用贷款",
|
||||||
|
"bank_name": "中国建设银行",
|
||||||
|
"object": "2",
|
||||||
|
"switch": 0,
|
||||||
|
"amount_min": null,
|
||||||
|
"amount_max": "3000000.00",
|
||||||
|
"term_min_months": null,
|
||||||
|
"term_max_months": 12,
|
||||||
|
"annual_rate_min": null,
|
||||||
|
"annual_rate_max": null,
|
||||||
|
"age_min": null,
|
||||||
|
"age_max": null,
|
||||||
|
"operating_months_min": null,
|
||||||
|
"regions": null,
|
||||||
|
"valid_from": null,
|
||||||
|
"valid_until": null,
|
||||||
|
"condition": "官网适用对象为优质纳税小微企业,要求按时足额纳税;企业主及配偶提供连带责任保证。官网未公开纳税额、纳税评级、经营月数或征信次数阈值,不能自行转成硬规则。",
|
||||||
|
"information": "官网未提供统一材料清单。准备建议:企业证照、经营与纳税资料、企业主及配偶身份和关系及保证相关材料;实际清单由受理分行确认。",
|
||||||
|
"recommendation": "官方说明为用于短期生产经营周转的可循环人民币信用贷款,额度最高300万元、期限最长1年,实际以审批和合同为准。",
|
||||||
|
"deprecated": "需要企业主及配偶连带责任保证,不能宣传为无担保贷款;不是云税贷,不能套用云税贷或其他分行未核实的利率及纳税门槛。",
|
||||||
|
"grade": "没有公开评分规则,不生成评分或通过率。",
|
||||||
|
"other": "公开材料草稿批次public-bank-materials-20260930-v1;查阅日期2026-09-30。来源:中国建设银行官网税易贷,网址:https://www2.ccb.com/chn/smallcompany/server/xyd/syd/index.shtml。网页顶部的重庆市为站点地区选择,正文未给贷款受理地域,不能据此设为重庆或全国。最低额度、最短期限、利率、有效期、完整准入条件与保证细则待核实,保持草稿。",
|
||||||
|
"eligibility_rules": {
|
||||||
|
"version": 1,
|
||||||
|
"source": {
|
||||||
|
"publisher": "中国建设银行",
|
||||||
|
"title": "小微企业产品服务:税易贷",
|
||||||
|
"reference": "https://www2.ccb.com/chn/smallcompany/server/xyd/syd/index.shtml",
|
||||||
|
"checkedOn": "2026-09-30"
|
||||||
|
},
|
||||||
|
"purposes": {"enterprise": ["working_capital"]},
|
||||||
|
"conditions": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
# 首批产品银行核实与验收
|
||||||
|
|
||||||
|
日期:2026-09-30。材料基线见[公开材料说明](首批产品材料-20260930.md)和[原始草稿JSON](首批产品草稿-20260930.json),机器缺项见[复核清单](首批产品复核清单-20260930.json)。本文件是待填写的核实表和验收准备,不是银行已确认的政策或签核记录。
|
||||||
|
|
||||||
|
当前四条均保持本机草稿。公开来源的查阅日期只证明当时查看过页面,不能替代当前执行政策、完整硬条件和具体受理范围的确认。目标城市尚待用户选择;当前系统支持深圳、广州、北京、上海、杭州,南京暂不支持。本轮没有联系银行、向外部发送材料或录入任何真实申请人身份信息。
|
||||||
|
|
||||||
|
机器复核结果:4条产品、0条格式完整、0条银行已确认、0条可发布;工银融e借8项、闪电贷7项、农银微捷贷5项、税易贷7项格式缺项。格式缺项只是当前后台发布检查的结果,不涵盖所有业务确认问题;每条仍有单独的银行问题。
|
||||||
|
|
||||||
|
## 1. 优先核实内容
|
||||||
|
|
||||||
|
| 产品 | 发布格式缺项 | 必须向受理银行确认的特殊问题 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 工银融e借 | 最低额度、最短期限、年化利率上下限、生效/失效日期、地区、细分用途,共8项 | 500万元/84个月分别适用何种渠道和客群;LPR浮动及加成机制;年龄例外;收入/资产/还款保障的替代关系 |
|
||||||
|
| 闪电贷线上材料 | 最低额度、最短期限、年化利率上下限、生效/失效日期、地区,共7项 | 南京报道中的30万元/84个月在目标地区、当前线上渠道是否适用;完整准入条件;医疗/购车/旅游的用途映射;不要混入线下50万元参数 |
|
||||||
|
| 农银微捷贷 | 最低额度、最短期限、年化利率上限、生效/失效日期,共5项 | 3.5%起是否仍有效、执行范围;实际受理分行;企业与法定代表人共同借款要求;其他完整准入条件 |
|
||||||
|
| 建行税易贷 | 最低额度、最短期限、年化利率上下限、生效/失效日期、地区,共7项 | 纳税及经营完整门槛;企业主及配偶连带责任保证;无配偶等情况的办理细则;不要套用云税贷政策 |
|
||||||
|
|
||||||
|
这27项是当前格式校验返回的独立字段问题。特殊问题属于业务确认,不能因不在格式清单里就忽略。共同借款、保证等要求也不能简化成有房、有公积金或固定收入阈值。
|
||||||
|
|
||||||
|
建议先取得农银微捷贷的当前受理材料作为第一条完整验收资料:现有公开稿用途、全国通用栏目和利率下限已有出处,格式缺项较少;共同借款和完整准入条件仍须单独确认。尚未决定正式首发产品或授权发布。
|
||||||
|
|
||||||
|
## 2. 银行回复填写表
|
||||||
|
|
||||||
|
每个产品复制本表填写一份。数值未知时填“银行未提供”,不要填0;期限用月、金额用元、年化利率用百分比。没有统一失效日期时应记录银行的实际表述和复核约定,不能自行编造有效期来通过校验;系统如何表达需要另行评估。
|
||||||
|
|
||||||
|
| 项目 | 待填写内容 |
|
||||||
|
| --- | --- |
|
||||||
|
| 对应产品/渠道/客群 | 待填写,注明线上/线下及产品版本 |
|
||||||
|
| 受理银行和分支机构 | 待填写 |
|
||||||
|
| 目标城市、受理区域 | 待填写;全国须有明确依据 |
|
||||||
|
| 确认日期、材料版本 | 待填写,区分网页查阅和银行回复日期 |
|
||||||
|
| 最低/最高额度(元) | 待填写,注明额度上限的适用条件 |
|
||||||
|
| 最短/最长期限(月) | 待填写,注明特定客群或渠道例外 |
|
||||||
|
| 年化利率下限/上限(%) | 待填写;注明单利/其他口径、浮动机制、当前适用日期 |
|
||||||
|
| 产品生效/失效日期 | 待填写;无统一有效期时记录原意 |
|
||||||
|
| 允许用途和禁止用途 | 待填写;不能把“其他用途”当作无限放开 |
|
||||||
|
| 完整准入条件 | 逐条填写字段、比较方式、阈值、单位、来源要求及全部例外 |
|
||||||
|
| 借款主体、保证/共同借款 | 待填写;明确主体及责任,不能只写“信用贷款” |
|
||||||
|
| 实际申请材料和授权渠道 | 待填写;区分必备、备选及特定渠道提额材料 |
|
||||||
|
| 条件依据 | 官方材料网址/编号/版本、对应页码或条款,不填私密链接令牌 |
|
||||||
|
| 运营核对人与核对日期 | 待填写,仅代表核对记录,不自动授权发布 |
|
||||||
|
| 当前系统无法核验的条件 | 待填写,注明采取保留说明/人工核验/暂不发布中的哪种处理,最终范围需确认 |
|
||||||
|
|
||||||
|
不要把申请人的证件、手机号、纳税明细、征信报告放进产品材料。这里只收集产品政策、参数及规则依据。
|
||||||
|
|
||||||
|
## 3. 当前表达能力差距
|
||||||
|
|
||||||
|
| 条件 | 当前能力 | 验收前需要决定的事项 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 收入、负债、经营月数等已采集数字 | 可配置数值比较,须有真实阈值依据 | 由银行提供阈值和适用对象;自填不等于核验 |
|
||||||
|
| 年龄/国籍/优质客户例外 | 当前没有对应匹配事实 | 不从身份证自行推导,不补新采集字段;取得政策后评估处理方式 |
|
||||||
|
| 收入或资产或其他还款保障的替代关系 | 当前条件组合是全部满足,不能完整表达此替代关系 | 不能将某一分支设为所有人的必需条件 |
|
||||||
|
| 企业与法定代表人共同借款 | 可保留说明,没有完整主体事实 | 明确人工核实和展示边界,不能自动宣称满足 |
|
||||||
|
| 企业主与配偶连带责任保证 | 可保留说明,没有保证落实事实 | 同上;不能因产品称信用贷款就显示无担保 |
|
||||||
|
| 纳税评级/足额纳税及银行其他条件 | 当前事实及核验接口不足 | 取得具体政策后判断是否有对应事实;不从企业信用代码推导 |
|
||||||
|
| 南京受理地区 | 当前五城市字典不含南京 | 保持待核实;未授权扩充地区字典 |
|
||||||
|
| 已取消的用户金额/期限/用途 | inputVersion=3不采集,也不作为匹配条件 | 产品参数仍录入展示;不能要求用户为匹配恢复填写 |
|
||||||
|
|
||||||
|
以上差距只记录本批已知问题,不能视为完整风险或完整银行条件清单。发布参数完整和条件可表达是独立门槛。
|
||||||
|
|
||||||
|
## 4. 验收样例
|
||||||
|
|
||||||
|
当前可执行的技术样例全部在内存中,不把四条公开草稿临时改成上架产品,也不构造真实银行“符合”的样例。
|
||||||
|
|
||||||
|
| 编号 | 场景 | 当前期望 | 验证方式 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| MAT-01—04 | 分别复核四条原始草稿 | 返回8/7/5/7项格式缺失;来源保留;仍待银行核实 | review_product_materials.php及product_materials.php |
|
||||||
|
| MAT-05 | 个人评估面对只有这四条草稿的目录 | unavailable / PRODUCT_CATALOG_UNAVAILABLE;无匹配项,计数为null | product_materials.php |
|
||||||
|
| MAT-06 | 企业评估面对相同草稿目录 | 同上,不虚构候选或四个unknown结果 | product_materials.php |
|
||||||
|
| TECH-01 | 合成个人月收入恰为8000.00元,合成门槛为不低于8000.00元 | eligible;没有已取消需求的证据;score=null | 仅合成技术样例,非任何银行门槛 |
|
||||||
|
| TECH-02 | 相同合成门槛,收入7999.99元 | ineligible;一分钱差值保留 | 同上 |
|
||||||
|
| TECH-03 | 相同合成门槛,收入缺失 | unknown;不把缺失当成0 | 同上 |
|
||||||
|
| TECH-04 | 合成条件要求verified,但收入只是用户自填 | unknown / SOURCE_INSUFFICIENT | 同上 |
|
||||||
|
| TECH-05 | 合成材料格式全部完整 | formatCheckPassed=true,mayPublish=false | 仅复核命令不签核、不发布 |
|
||||||
|
| TECH-06 | conditions写成{}而不是[] | 报格式错误,不能转换后通过 | 保留原JSON类型 |
|
||||||
|
|
||||||
|
取得银行确认后,需要由业务核对人给以下样例填写期望,再执行真实政策的金标准验收:
|
||||||
|
|
||||||
|
| 产品 | 待签核样例 | 银行业务期望/依据 | 核对记录 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 每个拟首发产品 | 全部可表达准入条件满足、每项关键条件刚好达到边界、每项刚好不满足、条件缺失/待核验、特殊主体/渠道/地区例外 | 待银行材料及业务核对,当前不预填eligible | 核对人/日期/材料条款待填写 |
|
||||||
|
| 含无法核验条件的产品 | 可表达部分满足,但共同借款/保证/年龄等未落实 | 待确定展示和人工核验处理,不能等同满足银行全部准入条件 | 待填写 |
|
||||||
|
|
||||||
|
## 5. 复核命令与后续处理
|
||||||
|
|
||||||
|
在后端目录D:/AAdesk/202609/airz运行:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
& 'D:/Develop/phpstudy_pro/Extensions/php/php7.4.3nts/php.exe' database/review_product_materials.php --materials 'D:/WorkSpace/cyun/ai融资/docs/产品材料/首批产品草稿-20260930.json' --as-of 2026-09-30
|
||||||
|
& 'D:/Develop/phpstudy_pro/Extensions/php/php7.4.3nts/php.exe' tests/product_materials.php 'D:/WorkSpace/cyun/ai融资/docs/产品材料/首批产品草稿-20260930.json'
|
||||||
|
```
|
||||||
|
|
||||||
|
复核命令只读取指定材料及既有规则,不初始化应用,不读取.env,不连接数据库、不访问来源网址、不调用模型。可使用--output指定新的JSON报告文件;已有文件和原材料不能被覆盖。技术预检可能通过,bankVerification仍是pending,mayPublish始终false。
|
||||||
|
|
||||||
|
收到银行回复后保留本次公开材料基线,另建确认后的材料版本;每一数值和条件须能对应原文条款。先确定无法表达条件和目标地区的处理方式,再更新本机草稿、重跑检查并核对人工期望。正式库录入、部署和上架另按明确范围执行;本文件不构成发布授权。
|
||||||
@@ -1,6 +1,10 @@
|
|||||||
# 原型04—10页面改造与跳转方案
|
# 原型04—10页面改造与跳转方案
|
||||||
|
|
||||||
版本:1.7。日期:2026-09-28。依据:[原型图](原型.jpg)、[开发文档](开发文档.md)、[数据契约](需求与评估结果数据契约.md)。本轮用户明确要求补齐04—10页面并随后进入产品匹配开发;此前“对比/PDF另行排期”的范围限制,自本方案起由下列正式任务覆盖。
|
版本:1.8。日期:2026-09-30。依据:[原型图](原型.jpg)、[开发文档](开发文档.md)、[数据契约](需求与评估结果数据契约.md)。本轮用户明确要求补齐04—10页面并随后进入产品匹配开发;此前“对比/PDF另行排期”的范围限制,自本方案起由下列正式任务覆盖。
|
||||||
|
|
||||||
|
2026-09-30最新覆盖:修改/新建与版本存储以[评估版本与最新资料](评估版本与最新资料.md)为准。09再次按原型还原为白底、顶部PDF、简洁状态区/摘要/编号改善项、固定底部重新评估;原下文中“重新评估一律空白新建”仅为历史,当前主按钮回填并修改同记录,独立新建在报告信息中提供。
|
||||||
|
|
||||||
|
同日产品展示更新:当前六条产品统一正常业务名称、机构和资料,03—10及PDF不展示环境标签,来源引用显示“产品规则档案”;匹配条件、参数和冻结结论保持。下文“明确标注演示”的要求已被用户最新要求覆盖。正式产品数据已同步,源码发布状态见[开发进度0.36](开发进度.md)。
|
||||||
|
|
||||||
## 1. 范围与落地原则
|
## 1. 范围与落地原则
|
||||||
|
|
||||||
@@ -63,10 +67,10 @@
|
|||||||
|
|
||||||
### 09 报告三栏与PDF
|
### 09 报告三栏与PDF
|
||||||
|
|
||||||
- 评估概况:冻结需求、完整度、三种准备状态、来源与生成时间、核验边界;不显示B+或审批概率。
|
- 评估概况首屏:浅绿/浅黄真实准备状态区、一段已保存摘要、最多三项实际改善建议,橙红圆形编号。详细完整度、事实、准备依据和核验边界移到“查看完整报告”;不显示B+或审批概率。v3取消采集的融资需求不出现在页面。
|
||||||
- 匹配结果:复用03的摘要与状态,最多三个候选、全部结果入口和产品解释跳转;没有候选时仍可查看不符/待核实原因。
|
- 匹配结果:复用03的摘要与状态,最多三个候选、全部结果入口和产品解释跳转;没有候选时仍可查看不符/待核实原因。
|
||||||
- 改善建议:由准备依据、已保存AI建议和产品证据组成;建议展示关联字段/产品,不保证改善后获批。没有依据时明确暂无额外建议,不凑三项。
|
- 改善建议:由准备依据、已保存AI建议和产品证据组成;建议展示关联字段/产品,不保证改善后获批。没有依据时明确暂无额外建议,不凑三项。
|
||||||
- 顶部下载PDF,底部“重新评估/下一步行动”;继续提供历史入口。三栏切换不触发新分析,刷新保留tab并重新读取本人报告。v1报告保留兼容提示,不补造v2指标。
|
- 顶栏下载PDF,底部固定“重新评估”;下一步行动/咨询AI为辅助入口,日期/版本/独立新建/历史入口在报告信息中。小程序标题和PDF避让胶囊,底部保留安全区。三栏切换不触发新分析,刷新保留tab和revisionId并重新读取本人报告。v1报告保留兼容提示,不补造v2指标。
|
||||||
- PDF从已保存报告及产品快照生成,不重跑DeepSeek或按现价替换历史值。包括需求、概况、匹配状态/候选、理由、建议、版本、生成时间和免责声明;不导出身份证、手机号、自由用途说明等身份内容。
|
- PDF从已保存报告及产品快照生成,不重跑DeepSeek或按现价替换历史值。包括需求、概况、匹配状态/候选、理由、建议、版本、生成时间和免责声明;不导出身份证、手机号、自由用途说明等身份内容。
|
||||||
- H5首版采用鉴权流式下载,无公开永久链接,不新增报告或导出表。需要的中文字体/PDF引擎在REPORT-005实施时做依赖评估并锁定;缺引擎返回明确不可用,不能发空PDF。
|
- H5首版采用鉴权流式下载,无公开永久链接,不新增报告或导出表。需要的中文字体/PDF引擎在REPORT-005实施时做依赖评估并锁定;缺引擎返回明确不可用,不能发空PDF。
|
||||||
- 用户主动下载时才生成文件;PDF失败允许重试,不影响已保存报告。微信内浏览器受限时给出可执行提示,不显示虚假下载成功;下载后本地文件由用户自行保管。
|
- 用户主动下载时才生成文件;PDF失败允许重试,不影响已保存报告。微信内浏览器受限时给出可执行提示,不显示虚假下载成功;下载后本地文件由用户自行保管。
|
||||||
|
|||||||
+45
-1
@@ -1,9 +1,53 @@
|
|||||||
# AI 融资平台开发文档
|
# AI 融资平台开发文档
|
||||||
|
|
||||||
> 版本:v1.24 · 基线日期:2026-09-22 · 更新日期:2026-09-29 · 状态:匹配分页/详情及公共产品详情已接通;04/05按原型还原并接通单产品DeepSeek解释,07对比与09报告三栏本地联调通过;08累计真实13例,金额精度连续三轮专项通过,真机/跨平台待验收;10行动本地联调通过;09PDF普通H5本地验证通过,平台/容量待验收
|
> 版本:v1.30 · 基线日期:2026-09-22 · 更新日期:2026-09-30 · 状态:正式库6条产品已同步正常业务文案,报告/PDF/助手去标签的3个PHP文件已正式部署并完成76项服务器验收,页面改动待发行;产品地区待核实、完整发布问题清单及既有编辑/公共目录本地联调完成,四条公开银行材料为隔离库草稿,未部署。最新资料/评估版本及会话任务列精简已完成正式库维护;小程序/H5未发布,外网HTTPS沿用此前连接重置状态。原型04—10及PDF既有功能保留。
|
||||||
> 配套台账:[开发进度](开发进度.md)。本文定义范围、接口和验收口径,台账记录实际执行与证据。
|
> 配套台账:[开发进度](开发进度.md)。本文定义范围、接口和验收口径,台账记录实际执行与证据。
|
||||||
> 文档初始规划后已开始开发;以下更新记录优先于原始基线。除明确列为已实现的部分外,计划接口和增量数据仍待开发。
|
> 文档初始规划后已开始开发;以下更新记录优先于原始基线。除明确列为已实现的部分外,计划接口和增量数据仍待开发。
|
||||||
|
|
||||||
|
### 0.36 2026-09-30 正常业务文案与来源展示
|
||||||
|
|
||||||
|
当前六条产品用于开发,按用户要求前台统一采用业务名称、机构显示名及正常材料/条件/建议,内部来源标记不作为可见标签。来源publisher/title为产品管理中心/产品参数与准入条件说明;稳定source reference不变,前端显示“产品规则档案”,产品/来源规则版本增加。金融参数、用途和准入条件保持,inputVersion=3不采集融资金额/期限/用途的约定不变;来源文字更新不新增银行核验结论。
|
||||||
|
|
||||||
|
正式产品数据已事务同步至ID22—27并备份,无新表/列,24张无关表一致。用户授权后,报告消息、助手提示词与PDF去标签的3个PHP文件已通过宝塔限定部署;76项服务器本机HTTPS/FPM和假模型检查通过,25表原有数据、其他源码/.env及全局参数保持,临时数据清理、worker与入口恢复,0次收费调用。备份及恢复入口见开发进度0.36和后端`database/product-display-update.md`;本次未包含产品编辑器/公共目录等其他未部署改动。
|
||||||
|
|
||||||
|
页面改动仍待发行。此前605项PHP、242项手机/桌面Playwright检查及微信开发编译通过;真实模型、真机与公网访问不属于本轮通过结果。此前强制显示环境标签的阶段说明已被本节覆盖。
|
||||||
|
|
||||||
|
### 0.35 2026-09-30 演示目录联调基线
|
||||||
|
|
||||||
|
正式库目前没有用户评估资料,保留6条带`demo:airz-`来源标识的演示产品供功能开发;它们不是银行产品,也不表示真实授信条件。匹配引擎按inputVersion=3的资料范围执行,产品自身额度、期限和用途仍仅作为冻结快照展示,已删除的用户需求字段不会生成证据或阻断条件。`database/seed_demo_products.php`支持显式本机隔离配置,不能用默认`.env`向正式库写入。
|
||||||
|
|
||||||
|
个人/企业三态和缺失边界回归已覆盖,产品目录测试按QA夹具筛选以隔离本机演示目录。正式银行材料仍按`local_draft_only`阻止上架,收到银行确认后须另建确认版本。
|
||||||
|
|
||||||
|
### 0.33 2026-09-30 待核实地区与发布诊断
|
||||||
|
|
||||||
|
产品编辑支持新增默认待核实(SQL NULL)、明确全国([])、指定城市列表;NULL不算旧规则。来源独立展示,真正旧规则未重建时只读查看并保留原JSON。发布前检查返回data.issues字段/问题列表,逐项诊断字段缺失、格式、范围、日期、用途和条件;诊断候选不可保存,无问题时仍走原完整编译器,保存和上架强校验不变。版本冲突、权限/数据范围及历史快照约定保持,无新增表列或迁移。450项PHP、89项真实本机HTTP/浏览器和7项数据完整性检查通过,详见进度0.33;四条公开材料来源及空值正常,仍缺银行确认参数和完整硬条件,全部保持本机草稿。未部署、未改正式库、无模型调用。
|
||||||
|
|
||||||
|
### 0.34 2026-09-30 材料复核与业务验收准备
|
||||||
|
|
||||||
|
只读`database/review_product_materials.php`直接加载纯内存规则和既有字典,不初始化应用或读取.env;按指定日期复核local_draft_only材料,输出字段缺项和待银行问题。`formatCheckPassed`只表示格式;bankVerification固定pending、mayPublish固定false,不代替签核或发布权限。--output只创建新的复核JSON,拒绝覆盖原材料或已有记录。首批共27项格式缺项,核实表和能力差距见[首批核实与验收](产品材料/首批产品银行核实与验收-20260930.md)。38项材料及合成资质匹配检查通过;真实政策、无法自动核验条件的处理和人工业务期望待确认。无需数据库字段/迁移,原输入v3和结果逻辑不变。
|
||||||
|
|
||||||
|
### 0.32 2026-09-30 产品后台与公共目录
|
||||||
|
|
||||||
|
PRODUCT-003录入工具已完成:结构化地区/用途/来源/条件编辑、草稿、只读发布检查、事务内_base_version冲突保护及上架/下架。公共GET /api/v1/products已实现会员鉴权、筛选、ID游标分页、当前发布资料投影和no-store,无个人匹配信息;完整口径见[产品匹配规则开发契约](产品匹配规则开发契约.md)第6.5及8.4节。未新增数据库字段/权限节点,历史快照及v3资料范围不变;326项PHP及89项本机HTTP/浏览器检查通过。未部署线上、未录入真实产品,真实资料及人工业务验收仍是任务完成门槛。
|
||||||
|
|
||||||
|
### 0.28 2026-09-30 最新资料与评估版本(优先)
|
||||||
|
|
||||||
|
最新数据设计、接口、并发与隐私约定统一见[评估版本与最新资料](评估版本与最新资料.md)。fa_user_info收敛为每用户每类型最新一条,任务移到fa_ai_chat,历史存fa_ai_chat_log.input_data单个JSON;新建增加会话,修改保留会话并增加版本。报告和匹配按版本冻结,成功才切换发布指针;本人编辑接口完整回填,公开展示/PDF/模型上下文仍脱敏,身份资料落库明文。
|
||||||
|
|
||||||
|
本轮已授权并完成线上迁移、配套PHP部署及63项维护验收,服务恢复;执行记录/备份/恢复边界在后端database/assessment-revisions.md及开发进度0.30节。此前“重新评估一律新增”“资料表一行一次评估”“身份加密”仅作历史。inputVersion=3的表单范围及resultVersion=2的结果样式继续保留;小程序/H5发行及外网访问恢复尚未完成。
|
||||||
|
|
||||||
|
### 0.27 2026-09-29 恢复原资料表单与报告清理
|
||||||
|
|
||||||
|
个人/企业信用融资恢复至2026-09-23 19:04需求扩展之前的采集范围:删除融资金额、融资期限、资金用途、其他用途说明;保留经营月数和原有身份/经营收入/资产负债/资料授权。提交使用inputVersion=3,结果仍为resultVersion=2,保留报告三栏、产品匹配、助手、PDF与行动入口。v3不生成需求事实或匹配规则,不把取消采集的字段列为“未提供/待核实”、补填项或完整度扣分项;产品自身额度、期限、利率和用途资料保留。其他真实缺失或来源未核验仍按既有规则处理。
|
||||||
|
|
||||||
|
已按用户授权在111.230.144.231/airz执行:删除报告161/229、关联会话151/219及2条日志;普通助手449及其2条日志保留,会员1个、产品6个保留。fa_user_info删除requested_amount、requested_term_months、purpose_code、purpose_detail四列;input_version、analysis_context属于必要版本和快照元数据,继续保留。两个触发器同步移除需求列校验,输入版本允许1/2/3,接口新建最低3。22张不相关表摘要一致,log_bin_trust_function_creators临时开启后恢复0。备份:D:/AAdesk/202609/airz-backups/restore-profile-20260929-180938/。
|
||||||
|
|
||||||
|
PHP十个生产文件已通过宝塔文件页面部署,php-fpm/Redis/airz-analysis恢复active,最后worker PID604113、NRestarts0(后续须重查)。站点仍/www/wwwroot/xctai.cn;代码与nginx备份及临时验收包已归档/root/airz-profile-20260929/。前端改动尚需重新发行并上传微信体验版,H5未发布。
|
||||||
|
|
||||||
|
当前外网阻塞:aitest.ychysg.cn已被腾讯云备案检查拦截,HTTP跳转dnspod.qcloud.com/static/webblock.html,外网HTTPS连接重置。服务器本机TLS与应用接口通过,不等于外网可用;体验版联网前需完成域名备案/接入并复测。此前“外网正常”的记录仅代表早先验收时间。
|
||||||
|
|
||||||
|
验证:53项资料表单专项、51项匹配兼容、29项PDF、18项助手回复、29项助手金额检查通过;服务器维护窗口22项实际HTTPS与假模型流程通过,测试用户/Token/任务已清理,无收费AI调用。微信最终编译通过;Android编译通过,未覆盖真机。H5通过Playwright受控响应检查个人/企业表单及v3提交、报告和对比;仅用于页面验证,不能代替外网或真实模型验收。
|
||||||
|
|
||||||
### 0.26 2026-09-29 宝塔 PHP 部署与域名切换
|
### 0.26 2026-09-29 宝塔 PHP 部署与域名切换
|
||||||
|
|
||||||
已按用户要求通过 Playwright 操作宝塔“网站→添加站点”部署 PHP。当前域名 https://aitest.ychysg.cn,API 前缀 /api/v1,后台 /ZYurKboadX.php;旧 xctai.cn 绑定已移除,内部站点名及目录仍 /www/wwwroot/xctai.cn,Web 运行目录为 public。新域名 DNS 指向 111.230.144.231,Let's Encrypt 证书已部署,2026-12-28 到期;HTTP 强制跳转 HTTPS,外网默认信任校验通过。面板续签任务正常、每天04:38执行,尚未经历到期续签。
|
已按用户要求通过 Playwright 操作宝塔“网站→添加站点”部署 PHP。当前域名 https://aitest.ychysg.cn,API 前缀 /api/v1,后台 /ZYurKboadX.php;旧 xctai.cn 绑定已移除,内部站点名及目录仍 /www/wwwroot/xctai.cn,Web 运行目录为 public。新域名 DNS 指向 111.230.144.231,Let's Encrypt 证书已部署,2026-12-28 到期;HTTP 强制跳转 HTTPS,外网默认信任校验通过。面板续签任务正常、每天04:38执行,尚未经历到期续签。
|
||||||
|
|||||||
+93
-3
@@ -1,8 +1,86 @@
|
|||||||
# AI 融资平台开发进度
|
# AI 融资平台开发进度
|
||||||
|
|
||||||
基线日期:2026-09-22。文档版本:1.24。最新规划及实施见0.26节与6.27节;此前章节保留阶段记录。技术方案和接口说明见[开发文档](./开发文档.md)。本台账现有63项任务,编号与开发文档保持一致。
|
基线日期:2026-09-22。文档版本:1.30。最新实施见0.36节;此前章节保留阶段记录。技术方案和接口说明见[开发文档](./开发文档.md)。本台账现有63项任务,编号与开发文档保持一致。
|
||||||
|
|
||||||
会话交接(2026-09-29):已整理[新会话交接](新会话交接.md),包含当前成果、验证边界、业务约定、代码入口和环境状态。原型08、10及09 PDF已完成本地实施,当前边界见0.26与6.27节;08累计13次真实调用,新增三轮金额精度专项通过;下一步为移动端、真实产品与跨平台验收。
|
### 0.36 2026-09-30 产品业务展示与正式资料同步
|
||||||
|
|
||||||
|
按用户要求,六条产品采用正常业务名称、机构显示名、材料、条件和建议,匹配/详情/对比/报告/行动/助手/PDF去除环境标签;内部来源引用和元数据保留。清理上一阶段未完成的内存模拟入口及登录绕过,维持真实后端链路。正式MySQL5.7/airz产品ID22—27已事务更新至版本2,参数/条件保持,其他24表逐行摘要一致。更新脚本、隔离恢复演练和正式六行备份见后端[恢复说明](D:/AAdesk/202609/airz/database/product-display-update.md)。没有DDL、历史改写或收费模型调用。
|
||||||
|
|
||||||
|
605项PHP检查通过(151匹配、153发布、166表单、45目录、22模型、38材料、30PDF),企业v3回归修正`year_income`映射,正确覆盖收入门槛和经营月数。Playwright/Chrome手机390×844、桌面1280×900完成242项检查,个人/企业报告→匹配分组→选择对比→展开→助手,以及产品来源/材料与行动入口正常,54次隔离后端GET,console/pageerror/HTTP错误0。微信开发编译通过;本轮Android未完成有效编译。截图和QA记录在临时`airz-product-editor-20260930`目录;联调账户、资料、会话、日志、Token和本批队列已清理,隔离24表恢复原摘要。
|
||||||
|
|
||||||
|
正式产品数据已同步;用户授权“先做第1项”后,后端`AssessmentNeedsReport.php`、`AssistantChats.php`、`ReportPdf.php`三处展示/提示词改动已通过宝塔备份、停空闲worker、限定部署并恢复服务。76项服务器本机HTTPS/FPM及假模型检查通过,实际PDF和助手流程正常,临时数据已清理,25张表原有数据与其他源码/.env保持;4次假客户端、0次收费调用。三服务active,最终worker PID944033/NRestarts0,队列为空,原nginx配置恢复。备份及恢复说明见上述后端文档;12个备份/验收文件已下载并核对源码/数据库SHA256。
|
||||||
|
|
||||||
|
本次未发布产品编辑器/公共目录的其他改动。小程序/H5未发行,前端需重新发行上传;公网HTTPS和真机未复验,本机TLS通过不表示公网恢复。本节覆盖0.35关于强制展示环境标签的旧约定。
|
||||||
|
|
||||||
|
### 0.35 2026-09-30 演示产品正式库核对与v3匹配回归
|
||||||
|
|
||||||
|
按用户确认,正式MySQL5.7/airz当前无评估用户,已有6条演示产品(个人、企业及混合主体)继续用于功能开发;逐条结构化比对与本地`demo_products.json`完全一致,未新增重复记录或改写历史会话。演示产品仍明确标记为非真实机构资料,仅用于页面、匹配和报告联调。
|
||||||
|
|
||||||
|
本机隔离MySQL5.7补齐同一批6条演示产品,保留原4条银行材料草稿;种子脚本新增`--env`显式隔离参数,重复执行只核对并保留已有记录。新增v3匹配回归覆盖个人/企业收入边界、城市范围、资料缺失三态、产品额度/期限保留、无评分以及已删除融资金额/期限/用途字段不进入证据。151项内存匹配、153项发布编译、45项目录模型、38项材料复核检查通过。
|
||||||
|
|
||||||
|
本轮没有新增正式库迁移、没有模型调用、没有部署前端;正式库6条演示产品可继续作为测试目录,真实银行材料仍保持草稿并阻止上架。后续优先联调评估→匹配→报告三条链路,再进行外网HTTPS与体验版真机验收。
|
||||||
|
|
||||||
|
### 0.33 2026-09-30 产品地区待核实与完整问题清单
|
||||||
|
|
||||||
|
按用户同意的下一步完善产品录入:新增“待核实”并作为新增默认,SQL NULL/明确全国[]/指定城市三态往返。NULL地区不再误判旧规则,四条公开材料来源正常显示;真正旧规则来源仍可只读查看,未明确重建时普通保存保留原JSON。发布检查一次列出独立字段缺失、范围倒置、过期或未来核对日期、用途及逐条条件错误;格式错误仅构造内存诊断候选,保存和模型/后台上架仍严格重验。空的可选经营月数不误报必填,版本冲突与规则版本上限继续阻断。
|
||||||
|
|
||||||
|
450项PHP检查通过(153发布、166表单、22模型、45目录、64规则),模型及目录合成写入全部回滚。82项真实本机HTTP/Playwright检查通过:四条材料只读预检、来源显示、完整清单、NULL保存/刷新、显式全国/城市、空城市、未知地区上架阻断、旧规则保留、版本冲突;1280×900和390×844均无横向溢出或新增console/pageerror。Browser plugin not available,使用既有Playwright,无新增依赖。7项清理/完整性检查通过:仅清理本轮两个合成产品,原ID1—4及user/user_info/ai_chat/ai_chat_log/model_user五表逐行摘要一致。
|
||||||
|
|
||||||
|
额外7项只读首屏/地区/来源检查通过,浏览器与HTTP共89项。证据在C:/Users/JUN/AppData/Local/Temp/airz-product-editor-20260930/qa-pending-ui.json、qa-pending-view.json、qa-pending-cleanup.json及pending-*.png;首次测试比较差异来自MySQL JSON格式化/键排序,已以插入后实际数据库文本作为基准复验。四条材料均保持本机草稿,不部署、不修改正式库、不调用模型、不扩充城市或资料采集。PRODUCT-003仍开发中,下一步取得受理银行对当前参数、有效期、地区及完整硬条件的确认,再做人工匹配金标准验收。
|
||||||
|
|
||||||
|
### 0.34 2026-09-30 首批材料复核与匹配验收准备
|
||||||
|
|
||||||
|
新增后端只读命令`database/review_product_materials.php`和内存检查`tests/product_materials.php`:固定读取`local_draft_only`材料JSON,复用ProductPublication检查发布格式,输出来源、地区三态、逐字段缺项、银行核实状态和不可发布标记;不初始化应用、不读取.env、不连接数据库、不访问来源网址、不调用模型、不覆盖原材料。材料格式完整仍不代表银行签核,`bankVerification=pending`且`mayPublish=false`。
|
||||||
|
|
||||||
|
首批四条复核结果:工银融e借8项、闪电贷7项、农银微捷贷5项、税易贷7项后台格式缺项;0条格式完整、0条银行确认、0条可发布。由于四条均是草稿,个人和企业profileOnly目录都返回`unavailable/PRODUCT_CATALOG_UNAVAILABLE`,不会把草稿伪装成四个unknown候选。合成样例补测金额边界、缺失三态、verified来源和已取消需求字段排除,38项检查通过。核实表及技术差距/人工金标准见`docs/产品材料/首批产品银行核实与验收-20260930.md`,复核JSON见同目录`首批产品复核清单-20260930.json`。
|
||||||
|
|
||||||
|
本轮仅准备核实材料、复核工具和合成技术样例,未取得新增银行回复或重新查阅官网;不进行银行联系、正式库写入、部署、上架、城市字典扩充或资料采集变更。收到确认后另建材料版本并完成无法核验条件的处理约定,才能开展真实政策的人工金标准验收;现有公开材料基线保持字节一致。
|
||||||
|
|
||||||
|
### 0.32 2026-09-30 PRODUCT-003结构化编辑与公共列表
|
||||||
|
|
||||||
|
按用户要求继续功能开发:后台产品新增/编辑已改为基本参数、全国/指定城市、按对象用途、来源四字段及动态准入条件;旧JSON默认保留,明确重建才替换规则并清除旧年龄条件。最多44条补充条件,金额保留元精度,字段/运算符/对象/来源均复用既有白名单。发布前只读检查、事务内版本冲突保护及工具栏上架/下架已接通,沿用FastAdmin权限和批量回滚,历史匹配快照不改写。新增GET /api/v1/products会员公共列表,按对象/城市/名称筛选、ID游标分页,响应no-store;复用公共详情DTO,无个人匹配信息。
|
||||||
|
|
||||||
|
326项PHP检查通过(73发布/64规则/128表单/16模型/45目录与编辑),本机MySQL5.7.26/PHP7.4.3隔离库合成写入回滚。另35项浏览器/HTTP主流程、18项旧规则/权限、36项公共目录真实本机HTTP检查通过。Playwright在1280×900、390×844验证非空页面、页面身份、无错误覆盖层、条件增删/切换回填、只读预检、发布/修改/冲突/下架、明确重建及只读管理员拒绝写入;console/pageerror均0。截图与无凭据QA记录在本机临时airz-product-editor-20260930目录。Browser plugin not available,使用既有Playwright依赖,无新增依赖安装。
|
||||||
|
|
||||||
|
本轮未迁移/修改正式库、未部署线上、无真实产品录入或模型调用;未改UTS,无新的小程序编译。PRODUCT-003保持开发中:录入工具已完成,真实资料/运营核对/金标准与正式发布验收仍依赖DEC-PRODUCT。下一步应准备首批真实产品条件材料,按契约核对后录入草稿并做人工匹配验收;公网HTTPS修复及体验版发行仍属独立待办。
|
||||||
|
|
||||||
|
收尾补充:10项生产静态资源/关闭调试模式检查通过,企业默认规则和移动布局正常;本机合成产品/会员/日志/Token已清理,仅保留隔离预览管理员。8097/3309预览服务与私密测试登录文件说明见交接文档,不与正式数据混用。
|
||||||
|
|
||||||
|
### 0.30 2026-09-30 正式评估版本迁移与PHP维护
|
||||||
|
|
||||||
|
已按用户本轮授权在111.230.144.231:3306/airz完成迁移及21个源码文件部署,MySQL5.7.44/PHP7.4.33。维护中先阻断外部写入、停worker并备份完整数据库和代码/.env/nginx,再迁移三表与部署。63项实际服务器本机HTTPS/FPM及假模型流程通过;原助手449及2日志、22张无关表摘要一致,仍1会员/6产品/0评估资料。合成数据/Token/任务清理,0次收费模型调用。
|
||||||
|
|
||||||
|
www恢复扫描为0,systemd实际消费失效的合成评估任务通过、队列为空、无storage error。PHP-FPM/Redis/airz-analysis恢复active,最终PID848445/NRestarts0;nginx原配置恢复,生产.env未变,log_bin_trust_function_creators恢复0。完整备份/root/airz-revisions-20260930,三表备份/root/airz-backups/assessment-revisions-20260930-115301-810221;详细过程及服务恢复后禁止整表回退的边界见后端database/assessment-revisions.md。
|
||||||
|
|
||||||
|
小程序/H5仍未发布,微信/Android编译为前轮验证。外网复测HTTP301跳转HTTPS,HTTPS仍连接重置;服务器本机TLS通过不代表外网恢复。下一步修复域名外网访问、核对微信合法域名,再发行体验版并做真机验收。本轮没有提交/推送。
|
||||||
|
|
||||||
|
### 0.31 2026-09-30 会话任务列精简
|
||||||
|
|
||||||
|
按用户确认将`fa_ai_chat.context_data`之后的17列精简为8列:`status`、`task_stage`、`task_id`、`lease_until`、`dispatched_at`、`type`、`current_input_id`、`published_input_id`。重复状态从当前输入日志的JSON读取,`match_result`、历史报告、普通助手不变。隔离MySQL5.7完成58项版本回归、精简迁移/备份恢复/重复拒绝演练及旧资料结构首次迁移;线上部署4个PHP文件并通过71项本机HTTPS/FPM假模型检查,worker实际消费失效合成任务后恢复。无收费模型调用。备份和恢复边界见后端`database/assessment-chat-compact.md`。
|
||||||
|
|
||||||
|
### 0.29 2026-09-30 原型09报告还原
|
||||||
|
|
||||||
|
按用户反馈再次对照原型09:白底/顶部PDF、浅色准备状态/简短摘要、实际改善项橙红编号列表、固定底部重新评估;详细完整度/事实/依据按需展开。保留真实准备状态,未引入B+评级、概率或固定建议。服务适配器从已有结构化improvements保留不含证据附注的正文,完整证据仍可展开;无API和数据库变更。
|
||||||
|
|
||||||
|
360/390/1280px H5受控响应验证、微信/Android编译通过,覆盖三栏/展开/空建议/旧版失败提示/PDF503回执与重试/原评估修改回填。PDF503为刻意注入,除此无新增console/pageerror;不是正式外网或真机验收。样例截图和脚本保留本机临时目录,未发布。
|
||||||
|
|
||||||
|
### 0.28 2026-09-30 最新资料与评估版本
|
||||||
|
|
||||||
|
已完成前后端代码:每用户两类最新资料、明文身份、同会话修改重评/新建独立评估、日志JSON版本、失败保留上次报告、匹配数组、版本历史分页以及报告/产品/对比/助手/行动/PDF绑定版本。本人修改完整回填,类型固定,baseRevisionId拒绝旧版本覆盖;并发原键重放不重复创建也不回写资料。完整契约见[评估版本与最新资料](评估版本与最新资料.md)。
|
||||||
|
|
||||||
|
MySQL5.7.26和MySQL8各51项隔离集成检查通过,5.7建表、合成迁移与恢复演练通过;微信/Android编译通过,H5真实本机接口联调修改和历史读取通过。全部使用隔离配置及假模型,不收费。旧任务测试尚未整体迁移,不能对新库/活动库直接运行。真机、真实模型语义和生产容量未验收。
|
||||||
|
|
||||||
|
本地开发完成,正式迁移/部署/小程序发布待执行;新客户端需配套新后端。迁移脚本、SHA256备份、阶段记录及维护期恢复说明见后端 `database/assessment-revisions.md`。本轮没有覆盖前轮未提交改动,也没有提交/推送。
|
||||||
|
|
||||||
|
会话交接(2026-09-29):已整理[新会话交接](新会话交接.md),包含当前成果、验证边界、业务约定、代码入口和环境状态。原型08、10及09 PDF已完成本地实施,当前边界见0.27与6.28节;08累计13次真实调用,新增三轮金额精度专项通过;下一步为移动端、真实产品与跨平台验收。
|
||||||
|
|
||||||
|
### 0.27 2026-09-29 恢复原资料表单
|
||||||
|
|
||||||
|
个人/企业信用融资恢复至2026-09-23 19:04需求扩展之前的采集范围:删除融资金额、融资期限、资金用途、其他用途说明;保留经营月数和原有身份/经营收入/资产负债/资料授权。提交使用inputVersion=3,结果仍为resultVersion=2,保留报告三栏、产品匹配、助手、PDF与行动入口。v3不生成需求事实或匹配规则,不把取消采集的字段列为“未提供/待核实”、补填项或完整度扣分项;产品自身额度、期限、利率和用途资料保留。其他真实缺失或来源未核验仍按既有规则处理。
|
||||||
|
|
||||||
|
已按用户授权在111.230.144.231/airz执行:删除报告161/229、关联会话151/219及2条日志;普通助手449及其2条日志保留,会员1个、产品6个保留。fa_user_info删除requested_amount、requested_term_months、purpose_code、purpose_detail四列;input_version、analysis_context属于必要版本和快照元数据,继续保留。两个触发器同步移除需求列校验,输入版本允许1/2/3,接口新建最低3。22张不相关表摘要一致,log_bin_trust_function_creators临时开启后恢复0。备份:D:/AAdesk/202609/airz-backups/restore-profile-20260929-180938/。
|
||||||
|
|
||||||
|
当前外网阻塞:aitest.ychysg.cn已被腾讯云备案检查拦截,HTTP跳转dnspod.qcloud.com/static/webblock.html,外网HTTPS连接重置。服务器本机TLS与应用接口通过,不等于外网可用;体验版联网前需完成域名备案/接入并复测。此前“外网正常”的记录仅代表早先验收时间。
|
||||||
|
|
||||||
## 0. 2026-09-22 首轮实施更新(优先于初始基线)
|
## 0. 2026-09-22 首轮实施更新(优先于初始基线)
|
||||||
|
|
||||||
@@ -513,7 +591,7 @@ DEC-DEMAND已确认并发布,需求录入/提交/历史子范围已实现;
|
|||||||
| --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- |
|
||||||
| PRODUCT-001 | 全栈开发(待指定) | 发布规则与人工样例可审阅;非法字段/操作符阻止发布;fail优先于unknown,匹配指标不作为审批概率。 | 首版字段/操作符/来源门槛已形成规则契约;64项内存测试;真实产品发布依据与业务样例待验收 | 2026-09-24 |
|
| PRODUCT-001 | 全栈开发(待指定) | 发布规则与人工样例可审阅;非法字段/操作符阻止发布;fail优先于unknown,匹配指标不作为审批概率。 | 首版字段/操作符/来源门槛已形成规则契约;64项内存测试;真实产品发布依据与业务样例待验收 | 2026-09-24 |
|
||||||
| PRODUCT-002 | 全栈开发(待指定) | 同输入/快照结果稳定;未知不当满足,配置错误不归咎用户;需求不拆额、不改期;未运行与零候选区分。 | 目录冻结/排序/理由已接正式worker;51项内存和19项任务测试通过;真实产品金标准、规模/生产验收仍待完成 | 2026-09-24 |
|
| PRODUCT-002 | 全栈开发(待指定) | 同输入/快照结果稳定;未知不当满足,配置错误不归咎用户;需求不拆额、不改期;未运行与零候选区分。 | 目录冻结/排序/理由已接正式worker;51项内存和19项任务测试通过;真实产品金标准、规模/生产验收仍待完成 | 2026-09-24 |
|
||||||
| PRODUCT-003 | 全栈开发(待指定) | 产品用途/地区/额度/期限有真实依据;规则可配置并启停,版本变化留依据;公共详情无个人分数。 | 发布预检及模型保存已实现;本地6条演示产品已授权上架,重复录入不覆盖;公共详情已接;真实资料/发布验收、结构化编辑和公共列表待完成 | 2026-09-24 |
|
| PRODUCT-003 | 全栈开发(待指定) | 产品用途/地区/额度/期限有真实依据;规则可配置并启停,版本变化留依据;公共详情无个人分数。 | 结构化编辑、来源/用途、只读预检、编辑冲突、上架/下架及公共列表本地联调通过,见0.32;真实资料/运营核对/发布验收待完成,6条演示产品不替代真实资料 | 2026-09-30 |
|
||||||
| PRODUCT-004 | 全栈开发(待指定) | 每条解释可追溯且不凑三正三反;当前产品变化不改历史;跨账户拒绝;unavailable/零候选/failed不混淆。 | 原型04/05和逐条DeepSeek解释本地完成;51项任务/输出检查、70项HTTP/快照检查与浏览器验证见6.18;真实机构资料及平台验收待完成 | 2026-09-28 |
|
| PRODUCT-004 | 全栈开发(待指定) | 每条解释可追溯且不凑三正三反;当前产品变化不改历史;跨账户拒绝;unavailable/零候选/failed不混淆。 | 原型04/05和逐条DeepSeek解释本地完成;51项任务/输出检查、70项HTTP/快照检查与浏览器验证见6.18;真实机构资料及平台验收待完成 | 2026-09-28 |
|
||||||
| PRODUCT-005 | 全栈开发(待指定) | 不跨评估/混用现价;缺值不同于0;手机横向表格可读,单项失败明确;不重跑模型。 | compare页面及6.16本地编译/真实读取与异常注入证据;真实机构产品和微信内H5/真机待验收 | 2026-09-28 |
|
| PRODUCT-005 | 全栈开发(待指定) | 不跨评估/混用现价;缺值不同于0;手机横向表格可读,单项失败明确;不重跑模型。 | compare页面及6.16本地编译/真实读取与异常注入证据;真实机构产品和微信内H5/真机待验收 | 2026-09-28 |
|
||||||
|
|
||||||
@@ -1215,3 +1293,15 @@ REPORT-005待验收。下一步08真实DeepSeek多轮语义、真实产品人工
|
|||||||
- PHP、Redis、airz-analysis均active/enabled,worker最后MainPID566317/NRestarts0、日志无storage error;本机analysis:work已停止。部署zip/_deploy移至root私密备份目录,不留公开校验入口。
|
- PHP、Redis、airz-analysis均active/enabled,worker最后MainPID566317/NRestarts0、日志无storage error;本机analysis:work已停止。部署zip/_deploy移至root私密备份目录,不留公开校验入口。
|
||||||
- 无新增数据库迁移,无业务源码改动、提交或推送。后端README及deployment-baota.md补充运维、兼容配置、备份和回退;前端H5未发布,下一步可对接新HTTPS API并继续移动端/跨平台验收。
|
- 无新增数据库迁移,无业务源码改动、提交或推送。后端README及deployment-baota.md补充运维、兼容配置、备份和回退;前端H5未发布,下一步可对接新HTTPS API并继续移动端/跨平台验收。
|
||||||
- 证据:C:/Users/JUN/AppData/Local/Temp/airz-bt-deploy-20260929/ 下 terminal-new-domain.png、terminal-final-domain-status.png、terminal-archive.png、aitest-admin-login.png。目录含私密部署配置,不整体共享。
|
- 证据:C:/Users/JUN/AppData/Local/Temp/airz-bt-deploy-20260929/ 下 terminal-new-domain.png、terminal-final-domain-status.png、terminal-archive.png、aitest-admin-login.png。目录含私密部署配置,不整体共享。
|
||||||
|
|
||||||
|
### 6.28 2026-09-29 原资料表单恢复验收
|
||||||
|
|
||||||
|
验证:53项资料表单专项、51项匹配兼容、29项PDF、18项助手回复、29项助手金额检查通过;服务器维护窗口22项实际HTTPS与假模型流程通过,测试用户/Token/任务已清理,无收费AI调用。微信最终编译通过;Android编译通过,未覆盖真机。H5通过Playwright受控响应检查个人/企业表单及v3提交、报告和对比;仅用于页面验证,不能代替外网或真实模型验收。
|
||||||
|
|
||||||
|
PHP十个生产文件已通过宝塔文件页面部署,php-fpm/Redis/airz-analysis恢复active,最后worker PID604113、NRestarts0(后续须重查)。站点仍/www/wwwroot/xctai.cn;代码与nginx备份及临时验收包已归档/root/airz-profile-20260929/。前端改动尚需重新发行并上传微信体验版,H5未发布。
|
||||||
|
|
||||||
|
当前外网阻塞:aitest.ychysg.cn已被腾讯云备案检查拦截,HTTP跳转dnspod.qcloud.com/static/webblock.html,外网HTTPS连接重置。服务器本机TLS与应用接口通过,不等于外网可用;体验版联网前需完成域名备案/接入并复测。此前“外网正常”的记录仅代表早先验收时间。
|
||||||
|
|
||||||
|
页面证据在仓库外C:/Users/JUN/AppData/Local/Temp/airz-profile-20260929/;数据库与服务器恢复说明见后端database/restore-profile-form.md。未提交/推送代码。
|
||||||
|
|
||||||
|
本轮H5验收环境:http://localhost:5173/,Chromium,390×844及1280×900;Browser插件不可用,使用现有Playwright CLI。个人/企业逐步填写→确认→合成提交均通过,捕获payload inputVersion=3且无四需求字段;个人收入0.800001万元精确提交8000.01元、企业经营月数0保留。报告页面身份/非空/无错误覆盖层正常,对比展开后无需求覆盖行,产品参数仍在;10项最终断言通过,console/pageerror均无错误,刷新表单为空。页面用受控响应,不调用真实远程POST或模型。
|
||||||
|
|||||||
+105
-3
@@ -1,6 +1,108 @@
|
|||||||
# AI融资项目新会话交接
|
# AI融资项目新会话交接
|
||||||
|
|
||||||
记录日期:2026-09-29。本文件为接续入口;详细任务状态以[开发进度](开发进度.md)0.26、6.27节为准,设计以[开发文档](开发文档.md)v1.24、[原型页面方案](原型04-10页面改造与跳转方案.md)v1.7为准。历史章节中的“待开发”应结合最新更新阅读。
|
记录日期:2026-09-30。本文件为接续入口;当前存储与流程以[评估版本与最新资料](评估版本与最新资料.md)、[开发进度](开发进度.md)0.36节及[开发文档](开发文档.md)v1.30为准。历史章节中的“待开发”应结合最新更新阅读。
|
||||||
|
|
||||||
|
## 最新产品展示:2026-09-30 正式数据及三文件PHP已部署
|
||||||
|
|
||||||
|
用户要求当前测试数据采用正常业务文案,不显示“演示、非真实”等环境标签。正式`111.230.144.231:3306/airz`现有6条产品ID22—27已精确更新为个人灵活消费贷、个人小额周转贷、个人优享信用贷、企业经营周转贷、企业成长经营贷、普惠基础融资贷,机构显示名及材料/条件/建议同步调整。所有金额、利率、期限、地区、准入条件及有效期保持,内部`demo:airz-`source reference保留。产品与来源规则版本由1增至2;未新增重复产品、未改写会话/历史报告,其他24表逐行摘要一致,0次模型调用。
|
||||||
|
|
||||||
|
新增后端`database/update_product_display.php`与[恢复说明](D:/AAdesk/202609/airz/database/product-display-update.md):显式目标、锁定六行、参数保护、写入前完整行备份、事务复验、重复执行无变更;隔离库已演练精确恢复。正式备份`D:/AAdesk/202609/airz-backups/product-display-20260930/product-display-20260930-180754-a4d94caa.json`。不得重新导入产生重复或整表恢复。
|
||||||
|
|
||||||
|
前端匹配/详情/报告/对比/行动/助手已移除环境标签,内部来源引用显示为“产品规则档案”;后端报告消息、PDF与助手提示词同步调整,继续按已配置参数及事实解释。已清理上一阶段未完成的前端内存模拟入口和登录绕过,走原真实接口。605项PHP检查与242项Playwright检查通过;390×844、1280×900覆盖个人/企业及展开/分组/选择对比/助手/材料/行动,54次隔离后端GET、错误0。微信开发编译通过,本轮Android未获得有效编译结果。联调账户/Token/评估/队列已精确清理,隔离库24表恢复原摘要,原4条材料草稿保留。证据位于临时`airz-product-editor-20260930/qa-display-ui.json`及`display-*.png`。
|
||||||
|
|
||||||
|
**正式产品数据及本轮三文件PHP已部署,小程序/H5未发行。** 用户授权“先做第1项”后,经宝塔上传部署`AssessmentNeedsReport.php`、`AssistantChats.php`、`ReportPdf.php`,备份并停空闲worker后仅替换3文件、重启PHP-FPM。76项服务器本机HTTPS/FPM及假模型检查通过,覆盖个人/企业报告、匹配、实际PDF、助手提示词和权限;4次显式假客户端、0次收费调用。临时对象已精确清理,25表原有数据逐行摘要、其他application源码、生产.env和数据库全局参数保持。原nginx恢复、三服务active,worker消费不存在的合成评估任务后队列为空;最终PID944033/NRestarts0,后续须重查。
|
||||||
|
|
||||||
|
服务器备份`/root/airz-product-display-20260930/`,12个备份/验收文件已下载到本机受限目录`D:/AAdesk/202609/airz-backups/product-display-deploy-20260930/`,原源码和完整数据库备份摘要一致;详细部署与三文件恢复边界见上述后端恢复说明。无数据库迁移,不重跑维护阶段、不整库回退。本次未发布产品编辑器/公共目录的其他本地改动,前端仍需重新发行。本机5173 H5、8097隔离PHP及3309隔离MySQL沿用上一轮状态,继续先核对监听;不要启动默认本机worker。公网HTTPS阻塞未复测,本机TLS通过不表示公网恢复。下节关于强制显示环境标签的表述为已被用户最新要求覆盖的历史记录。
|
||||||
|
|
||||||
|
## 历史产品联调:2026-09-30 正式库演示数据核对
|
||||||
|
|
||||||
|
用户确认正式库也使用当前演示数据,且暂时没有用户。已只读核对正式`111.230.144.231:3306/airz`:6条演示产品与`database/demo_products.json`逐字段一致,正式产品ID为22—27;没有新增重复记录、没有改写会话或报告。产品名称、机构和来源均明确为演示/非真实金融资料,只供功能开发与验收,不代表银行政策或授信承诺。
|
||||||
|
|
||||||
|
本机隔离库`127.0.0.1:3309/airz_product_test_20260930`已通过`database/seed_demo_products.php --env <隔离配置> --apply`补齐同一批6条,原4条银行材料草稿保持不变;重复运行返回`kept_existing`。新增v3匹配回归151项、发布编译153项、产品目录45项、材料复核38项通过。正式库不需要再次导入这批数据;本机默认`.env`仍指向正式库,任何本地种子/测试必须显式使用隔离配置。
|
||||||
|
|
||||||
|
后续继续评估→匹配→报告链路联调时,个人/企业均可使用这些演示产品覆盖符合、条件不符和待核实三态;真实银行四条草稿仍不可上架,收到确认后另建材料版本。
|
||||||
|
|
||||||
|
## 最新产品编辑改进:2026-09-30 待核实与发布检查
|
||||||
|
|
||||||
|
用户同意实施下一步第1项,本机开发已完成:新增默认“待核实”地区,保存SQL NULL;明确全国才保存[],指定城市非空列表。NULL地区不再误判旧规则,四条公开材料来源正常显示;真正旧规则的已有来源仍可只读查看,未明确重建时普通保存保留原JSON。发布前检查一次返回data.issues字段/问题列表,覆盖独立字段缺失/错误、范围倒置、日期、用途及逐条条件;可选经营月数空值不误报必填。诊断候选仅在内存中使用,真实保存/模型或后台上架仍走严格完整校验,未知地区不能上架。版本冲突及规则版本上限保持。
|
||||||
|
|
||||||
|
450项PHP、89项HTTP/Playwright(含7项只读首屏检查)、7项清理/完整性检查通过,1280×900及390×844无横向溢出或新增console/pageerror。合成产品已精确清理,原ID1—4和五张业务表逐行摘要不变;无正式库修改、部署或模型调用,无UTS改动。证据在临时airz-product-editor-20260930目录的qa-pending-ui.json、qa-pending-view.json、qa-pending-cleanup.json及pending-*.png。首次比较差异来自MySQL JSON格式化,已按插入后的实际存储文本复验;没有旧规则被改写。
|
||||||
|
|
||||||
|
PHP8097/MySQL3309隔离服务仍运行,监听127.0.0.1,模型关闭、未启动worker;预览入口沿用下节,下次先复查监听及临时bootstrap。项目.env仍是正式库,不能直接启动默认服务或旧集成测试。不要运行旧cleanup.php/reset-products.php删除四条材料。下一步仍需受理银行确认当前参数/地区/有效期/完整硬条件,再做人工匹配验收;本轮代码未部署。
|
||||||
|
|
||||||
|
## 最新材料复核:2026-09-30
|
||||||
|
|
||||||
|
已新增只读材料复核命令`D:/AAdesk/202609/airz/database/review_product_materials.php`和`tests/product_materials.php`。复核首批隔离草稿不连接数据库、不读取.env、不访问来源网址、不调用模型,并输出[银行核实与验收表](产品材料/首批产品银行核实与验收-20260930.md)及[复核清单](产品材料/首批产品复核清单-20260930.json)。四条仍为草稿:工银融e借8项、闪电贷7项、农银微捷贷5项、税易贷7项格式缺项;0条格式完整、0条银行确认、0条可发布。个人/企业profileOnly面对当前草稿目录均返回`unavailable/PRODUCT_CATALOG_UNAVAILABLE`,不虚构匹配候选。38项材料/匹配准备检查通过。收到银行回复前不能把公开参数改成当前政策、扩充南京城市或上架;真实材料需另建确认版本,保留本次公开基线。
|
||||||
|
|
||||||
|
本轮未重新查阅官网或取得银行回复;目前目标城市及当前银行政策材料仍待用户补充。核实表给出每产品特殊问题、可填写的统一回复表及规则表达能力差距;真实银行样例期望尚待签核,合成收入8000元门槛只用于技术测试,不能套用于四条产品。未改应用运行逻辑、数据库或UTS,未部署或调用模型。
|
||||||
|
|
||||||
|
## 最新产品材料:2026-09-30 本机草稿
|
||||||
|
|
||||||
|
用户要求准备几个产品材料录入。本轮查阅银行官网,整理工银融e借、闪电贷线上公开材料、农银微捷贷、建行税易贷四条,在127.0.0.1:3309/airz_product_test_20260930保存草稿ID1—4;没有改写正式库、既有演示产品或历史报告,没有上架、部署、模型调用或源码改动。材料、原文依据、准备建议和待核实事项见[首批产品材料](产品材料/首批产品材料-20260930.md),结构化元字符串见同目录JSON。
|
||||||
|
|
||||||
|
所有未知最低额度、最短期限和有效期保留NULL;微捷贷官网长图的3.5%起只记录下限,上限未知。官网全国通用栏目仅支持微捷贷regions=[];其余三条地区NULL,闪电贷南京分行材料不能外推全国,南京也不在现有城市字典内。后续编辑器已支持“待核实”,NULL地区不再需要重建,来源正常显示;未核实地区前不要改成全国。四条均缺发布参数,模型发布检查全部阻止上架。信用良好等非量化条件、年龄例外、共同借款及连带保证保留说明,不补造机器规则。正式匹配前还需银行确认当前政策与完整硬条件,并验证系统表达能力。
|
||||||
|
|
||||||
|
本机MySQL/PHP原进程已停止,本轮用原my.ini及router/bootstrap恢复3309/8097,均监听127.0.0.1,分析模型禁用,未启动worker。后台列表显示四条草稿,微捷贷编辑页来源及空值正常;重复执行只核对已有记录,不产生重复,会员/资料/会话/日志/用量表计数未变。录入脚本及无凭据回执public-materials-import.json、四个官网来源截图和public-materials-catalog.png位于C:/Users/JUN/AppData/Local/Temp/airz-product-editor-20260930。旧cleanup.php/reset-products.php会删除本机产品,不要再用于本批材料。继续仍先检查监听,不假定旧PID有效;后台地址和测试管理员沿用下节。
|
||||||
|
|
||||||
|
## 最新功能开发:2026-09-30 产品后台
|
||||||
|
|
||||||
|
用户暂不处理公网HTTPS,转而继续PRODUCT-003。本轮完成本地产品结构化编辑、来源/用途/城市控件、动态准入条件、只读发布检查、版本冲突和上架/下架工具栏;补GET /api/v1/products会员公共目录并复用详情DTO。不新增数据库字段/权限节点,不改UTS,不重写历史匹配结果。旧规则默认保存原JSON,明确重建才替换及清除旧年龄条件;发布资料完整性不等于来源真实性。
|
||||||
|
|
||||||
|
入口:后端ProductEditor.php/ProductCatalog.php、admin Product控制器/模型、product/form.html及backend/product.js、API v1 Products与路由。保存时锁定最新行检查_base_version,后台入口固定走结构化映射,删除客户端标记不能绕过;source/purposes/conditions变化才增加规则版本。匹配v3不采集金额/期限/用途的约定保持,产品自身范围仍需真实录入。
|
||||||
|
|
||||||
|
326项PHP检查与89项真实隔离HTTP/浏览器检查通过;Playwright桌面1280×900、手机390×844,console/pageerror均0,无收费模型调用。本机测试库airz_product_test_20260930,127.0.0.1:3309/MySQL5.7.26;8097 PHP7.4.3使用临时router/bootstrap覆盖配置,模型禁用。证据及脚本在C:/Users/JUN/AppData/Local/Temp/airz-product-editor-20260930。项目.env仍连接正式远程库,禁止直接启动默认PHP服务/worker或运行旧集成测试。新的tests/product_publication_model.php与product_catalog.php必须显式传本机airz_product_test_*配置;纯内存product_editor.php可直接运行。
|
||||||
|
|
||||||
|
本轮未进行线上写入或部署,也未收到真实产品材料;PRODUCT-003仅工具与接口完成,真实产品发布/业务金标准仍待验收。继续时优先准备真实材料并核对录入草稿,不能把演示产品改名当成真实产品,不得延用旧生产迁移授权发布本轮代码。
|
||||||
|
|
||||||
|
收尾额外10项关闭调试/生产静态资源模式检查通过,企业默认条件、金额输入、响应式布局和资源加载正常。合成产品、会员、日志与Token已清理,业务六表为0;仅保留一个本机测试管理员便于预览,登录信息在临时fixture.private.json,不含生产凭据或会员令牌。8097隔离PHP及3309临时MySQL保留运行,模型禁用、监听127.0.0.1;预览地址http://127.0.0.1:8097/ZYurKboadX.php/index/login?url=product%2Findex。这些进程会随终端/任务环境变化停止,下次先复查端口;不得用项目默认.env替代临时bootstrap。临时目录可能被系统清理,仓库中的代码/测试/契约才是持久交付。
|
||||||
|
|
||||||
|
## 最新线上维护:2026-09-30 已完成
|
||||||
|
|
||||||
|
本轮后续维护已完成:`fa_ai_chat.context_data`后由17列精简为8列,迁移前已阻断入口、停worker、完整备份;独立脚本同步当前输入JSON状态后删9列,普通助手/历史结果保留。MySQL5.7隔离迁移与恢复演练通过;线上4个PHP文件部署,71项本机HTTPS/FPM假模型检查通过,23张无关表、原数据和私密环境逐行一致,worker实际消费失效合成任务后恢复,0次收费调用。服务器备份:`/root/airz-backups/assessment-chat-compact-20260930-131231-873749/`,恢复说明见后端`database/assessment-chat-compact.md`。维护末尾探针已复查线上为8列;外网HTTPS仍连接重置,小程序/H5未发行。
|
||||||
|
|
||||||
|
用户本轮授权继续迁移与恢复方案,已完成111.230.144.231:3306/airz(服务器.env连接127.0.0.1)的评估版本正式迁移、21个配套源码文件部署、维护验收并恢复服务。MySQL5.7.44-log、PHP7.4.33;PHP-FPM/Redis/airz-analysis均active,最终worker PID848445/NRestarts0,后续重查。生产.env逐字未变,nginx原配置恢复,log_bin_trust_function_creators恢复0。
|
||||||
|
|
||||||
|
63项真实服务器本机HTTPS/FPM和假模型检查通过:新建独立会话、修改复用并保存新JSON版本、失败保留旧报告、历史报告/匹配/助手/PDF、最新个人企业资料上限、鉴权/越权及Token撤销。www恢复扫描为0,systemd实际消费已不存在的合成任务通过;无storage error、队列为空,0次收费模型调用。合成用户/资料/会话/日志/Token/任务已清理。维护前无评估资料和报告,原普通助手449及两日志按旧列逐行摘要一致;22张无关表一致,仍1会员、6产品。
|
||||||
|
|
||||||
|
完整数据库、代码/.env/nginx及验收备份:`/root/airz-revisions-20260930/`;三表迁移备份:`/root/airz-backups/assessment-revisions-20260930-115301-810221/`。详细过程与恢复边界见后端`database/assessment-revisions.md`。正式写入已恢复,禁止整表恢复旧备份或换回旧PHP丢失新增数据,不得重跑迁移或旧测试。本机worker仍不要连接活动库。
|
||||||
|
|
||||||
|
完整备份及验收摘要10个文件已下载到本机受限目录`D:/AAdesk/202609/airz-backups/assessment-revisions-production-20260930/`,数据库gzip和代码tar.gz的SHA256均与服务器一致。无凭据final-summary.json保存实际结构、服务和63项检查结果;完整备份含私密环境和身份信息,不对外分享。临时运维脚本/截图在`C:/Users/JUN/AppData/Local/Temp/airz-maintenance-20260930/`,不是公开应用代码。
|
||||||
|
|
||||||
|
前端原型09及版本流程此前已编译通过,**小程序/H5尚未发布**。体验版配置已有https://aitest.ychysg.cn/api/v1。外网本次HTTP301跳转正确HTTPS,但HTTPS仍Connection reset;此前备案阻断是历史证据,尚未确认当前外网重置原因。下一步排查外网访问、核对微信request合法域名,恢复后发行体验版并完成真机流程;服务器本机TLS通过不代表外网可用。
|
||||||
|
|
||||||
|
## 最新界面调整:2026-09-30 原型09还原
|
||||||
|
|
||||||
|
用户反馈报告与原型09差异较大,已重新对照原型图实施:白底、顶栏下载PDF、三栏、浅绿准备状态区(需补充时浅黄)、摘要正文、橙红编号重点改善项、固定底部“重新评估”。完整度/逐项事实/状态依据/核验边界移到“查看完整报告”,建议证据可展开,报告信息/独立新建/历史入口弱化。使用真实状态和实际建议,不补造B+或固定三条。重新评估仍修改同记录,报告版本/导出绑定及旧版失败提示保留;本轮仅前端显示调整,未迁移/部署。
|
||||||
|
|
||||||
|
页面验证使用现有Playwright CLI(Browser plugin not available),localhost:5173受控响应来自隔离合成报告,视觉样例摘要/三条建议仅在测试响应中设置,没有改写数据库。360×844、390×844、1280×900验证首屏/固定按钮、详情展开、三栏、空建议、旧版失败提示、PDF503失败回执/重试成功及修改回填。除刻意注入的PDF503外无新增console/pageerror;截图 `C:/Users/JUN/AppData/Local/Temp/airz-revisions-20260929/report09-after-390.png` 等。微信/Android本地编译通过,微信胶囊和底部安全区已做布局处理,真机未验收。PHP/数据库/结果计算不变,正式发布状态仍见下节。
|
||||||
|
|
||||||
|
## 2026-09-30 评估版本与最新资料实现记录
|
||||||
|
|
||||||
|
已完成:fa_user_info每用户个人/企业各最多一条最新资料,保留企业名称/信用代码/经营月数,city_code在city前,delete_time末列;业务身份明文保存。新建评估新增fa_ai_chat,修改重评复用原会话并在fa_ai_chat_log.input_data增加JSON输入版本;最新成功匹配仍存fa_ai_chat.match_result数组。任务状态移到会话,历史输入/报告/匹配冻结在日志。无新业务表。
|
||||||
|
|
||||||
|
前端已接本人完整回填、提交版本冲突保护、档案版本分页、历史报告/匹配/对比/助手/行动/PDF及登录恢复。处理中或失败保留上次成功结果;历史解释只读,旧助手不切换上下文。关键代码已加中文注释。
|
||||||
|
|
||||||
|
验证:MySQL5.7.26和MySQL8隔离库各51项版本集成检查通过(含两个独立连接并发新建/修改);5.7真实建表、合成迁移及备份恢复演练通过;微信/Android编译通过。H5在localhost:5173通过真实隔离后端8097联调修改回填、旧输入/报告/PDF/匹配/助手;假模型,不收费。未覆盖真机与正式模型语义。前轮历史验证数仅代表当时版本,旧任务测试依赖旧表结构不可对新版库直接运行。
|
||||||
|
|
||||||
|
本地阶段当时未发布;本轮用户授权后,正式迁移与PHP部署现已完成,详见顶端线上维护记录,小程序尚未上传。当前后端.env仍指远程正式库,不要直接启动本机worker或运行旧集成测试。迁移脚本和完整恢复方案位于 `D:/AAdesk/202609/airz/database/migrate_assessment_revisions.py`、`database/assessment-revisions.md`,本轮迁移不得重跑。
|
||||||
|
|
||||||
|
以下2026-09-29部署/表单恢复均为历史和本次迁移前基线;本轮结构是否上线以上方最新维护记录为准。正式域名外网HTTPS仍连接重置,本机联调不代表外网恢复。
|
||||||
|
|
||||||
|
收尾补测:390×844和1280×900下历史报告→匹配→对比→助手、行动页→修改回填、退出重进通过,最终console/pageerror均无新增错误;修复修改页onHide创建提示文本引起的attachVmProps错误。截图与临时脚本保留在 `C:/Users/JUN/AppData/Local/Temp/airz-revisions-20260929/`。合成UI用户/Token/队列已清理,8097临时PHP及3309临时MySQL5.7已停止,隔离库文件和恢复备份保留。自动审批策略拦截了清理Python缓存的命令,后端未跟踪 `database/__pycache__/` 保留,不属于交付代码。
|
||||||
|
|
||||||
|
## 最新优先状态:2026-09-29 恢复原资料表单
|
||||||
|
|
||||||
|
个人/企业信用融资恢复至2026-09-23 19:04需求扩展之前的采集范围:删除融资金额、融资期限、资金用途、其他用途说明;保留经营月数和原有身份/经营收入/资产负债/资料授权。提交使用inputVersion=3,结果仍为resultVersion=2,保留报告三栏、产品匹配、助手、PDF与行动入口。v3不生成需求事实或匹配规则,不把取消采集的字段列为“未提供/待核实”、补填项或完整度扣分项;产品自身额度、期限、利率和用途资料保留。其他真实缺失或来源未核验仍按既有规则处理。
|
||||||
|
|
||||||
|
已按用户授权在111.230.144.231/airz执行:删除报告161/229、关联会话151/219及2条日志;普通助手449及其2条日志保留,会员1个、产品6个保留。fa_user_info删除requested_amount、requested_term_months、purpose_code、purpose_detail四列;input_version、analysis_context属于必要版本和快照元数据,继续保留。两个触发器同步移除需求列校验,输入版本允许1/2/3,接口新建最低3。22张不相关表摘要一致,log_bin_trust_function_creators临时开启后恢复0。备份:D:/AAdesk/202609/airz-backups/restore-profile-20260929-180938/。
|
||||||
|
|
||||||
|
PHP十个生产文件已通过宝塔文件页面部署,php-fpm/Redis/airz-analysis恢复active,最后worker PID604113、NRestarts0(后续须重查)。站点仍/www/wwwroot/xctai.cn;代码与nginx备份及临时验收包已归档/root/airz-profile-20260929/。前端改动尚需重新发行并上传微信体验版,H5未发布。
|
||||||
|
|
||||||
|
当前外网阻塞:aitest.ychysg.cn已被腾讯云备案检查拦截,HTTP跳转dnspod.qcloud.com/static/webblock.html,外网HTTPS连接重置。服务器本机TLS与应用接口通过,不等于外网可用;体验版联网前需完成域名备案/接入并复测。此前“外网正常”的记录仅代表早先验收时间。
|
||||||
|
|
||||||
|
验证:53项资料表单专项、51项匹配兼容、29项PDF、18项助手回复、29项助手金额检查通过;服务器维护窗口22项实际HTTPS与假模型流程通过,测试用户/Token/任务已清理,无收费AI调用。微信最终编译通过;Android编译通过,未覆盖真机。H5通过Playwright受控响应检查个人/企业表单及v3提交、报告和对比;仅用于页面验证,不能代替外网或真实模型验收。
|
||||||
|
|
||||||
|
恢复与旧脚本禁用说明见后端database/restore-profile-form.md。以下部署、历史报告保留及旧输入版本记录仅作历史追溯,与本节冲突时以本节为准。
|
||||||
|
|
||||||
## 1. 当前停在哪
|
## 1. 当前停在哪
|
||||||
|
|
||||||
@@ -40,8 +142,8 @@ tests/assistant_semantics.php支持--boundaries定向4例、--money-precision同
|
|||||||
|
|
||||||
## 3. 必须延续的业务约定
|
## 3. 必须延续的业务约定
|
||||||
|
|
||||||
- 不做草稿;重新评估进入空白表单,不覆盖历史评估。
|
- 不做草稿;新建评估进入空白表单并新增会话,修改评估回填本评估最新提交并复用会话、增加版本。
|
||||||
- fa_user_info一条对应一次评估;fa_ai_chat保存关联报告及匹配结果;fa_ai_chat_log保存消息和调用记录。不新建独立评估/报告/匹配表。
|
- fa_user_info每用户每类型仅存最新资料;fa_ai_chat保存独立评估及最新成功报告/匹配数组;fa_ai_chat_log.input_data保存历史输入与该版结果,原消息和调用日志保留。不新建独立评估/报告/匹配表。
|
||||||
- Token保存在Redis,fa_user_token已删除。
|
- Token保存在Redis,fa_user_token已删除。
|
||||||
- 金额数据库DECIMAL(11,2),单位元;API用元十进制字符串;客户端万元,保留精度,例如123456.78元显示12.345678万元。
|
- 金额数据库DECIMAL(11,2),单位元;API用元十进制字符串;客户端万元,保留精度,例如123456.78元显示12.345678万元。
|
||||||
- 产品稳定条件、额度/期限/利率等使用独立字段,地区/补充规则JSON;沿用冻结产品快照,不用当前产品参数覆盖历史。
|
- 产品稳定条件、额度/期限/利率等使用独立字段,地区/补充规则JSON;沿用冻结产品快照,不用当前产品参数覆盖历史。
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
# 评估版本与最新资料
|
||||||
|
|
||||||
|
2026-09-30:本地代码已实施,正式数据库迁移、配套PHP部署及维护验收已完成,服务恢复;小程序/H5尚未发布。本文覆盖旧文档中“一条资料代表一次评估”“重新评估一律空白新建”“身份证落库加密”的历史约定。会话任务列已精简为8列,逻辑状态继续保存在版本输入JSON中。
|
||||||
|
|
||||||
|
## 用户行为
|
||||||
|
|
||||||
|
- **发起新评估**:空白个人/企业表单,正式提交新增一条 `fa_ai_chat`。
|
||||||
|
- **修改资料并重新评估**:回填所选评估最新提交的完整资料,类型不可切换;提交复用原 `fa_ai_chat`,增加一个输入版本。不把另一份较新的独立评估资料混入本评估。
|
||||||
|
- 不保存草稿。正式提交时按用户和类型更新最新资料;分析失败不撤销此次正式提交。每用户在 `fa_user_info` 最多个人、企业各一条。
|
||||||
|
- 融资档案保留独立评估列表,并提供本评估版本历史和分页;历史输入与结果可单独查看。
|
||||||
|
- 重评处理中或失败保留上次成功报告;成功才整体发布新报告与匹配结果。旧版本不被后续修改覆盖。
|
||||||
|
- 继续使用inputVersion=3表单,不采集融资金额/期限/用途,也不显示这些取消字段的“未提供/待核实”。结果格式仍为resultVersion=2,不添加虚构分数。
|
||||||
|
|
||||||
|
## 存储及版本契约
|
||||||
|
|
||||||
|
| 表 | 约定 |
|
||||||
|
| --- | --- |
|
||||||
|
| `fa_user_info` | 最新正式资料,唯一 `(user_id,type)`;企业名称、信用代码、经营月数保留,`city_code` 在 `city` 前,末列 `delete_time` |
|
||||||
|
| `fa_ai_chat` | 独立评估及当前任务;`current_input_id` 最新提交,`published_input_id` 最新成功版;`match_result` 为匹配对象数组,含03/04页面所需冻结产品、规则证据及已有解释 |
|
||||||
|
| `fa_ai_chat_log.input_data` | 单个JSON存该版form/state/documents/consents/analysisContext/report/matches;system行作为版本,原聊天和模型调用记录继续保留 |
|
||||||
|
|
||||||
|
`revisionId` 是日志ID字符串,`revisionNo` 是同一评估内从1开始的展示序号;均不等于inputVersion/resultVersion。`assessmentId` 为公开评估编号,迁移保留旧值,新建使用会话ID;不得再用它查询最新资料表主键。
|
||||||
|
|
||||||
|
JSON结构:`schemaVersion:1, revisionNo, form, state, editId, baseRevisionId, documents, consents, report, matches, analysisContext`。form使用规范数据库字段,金额为元字符串;客户端展示万元,保留六位精度。迁移额外保存legacyProfile以保留旧列值。
|
||||||
|
|
||||||
|
身份证等业务身份资料按用户要求明文保存;完整回填仅本人鉴权接口可读。展示/PDF/模型白名单脱敏继续执行,业务明文要求不涉及登录密码、系统密钥和Token。
|
||||||
|
|
||||||
|
## 接口和并发
|
||||||
|
|
||||||
|
接口前缀 `/api/v1`:
|
||||||
|
|
||||||
|
| 接口 | 含义 |
|
||||||
|
| --- | --- |
|
||||||
|
| `POST /assessments` | 新建独立评估,requestId原键重放不新增、不回写最新资料 |
|
||||||
|
| `GET /assessments/:id/edit` | 本人最新提交版本的完整可编辑表单及baseRevisionId |
|
||||||
|
| `POST /assessments/:id/revisions` | 提交修改,携带baseRevisionId与独立requestId;版本冲突拒绝,处理中拒绝新修改 |
|
||||||
|
| `GET /assessments/:id/revisions` | 同评估版本分页,默认20条 |
|
||||||
|
| 详情、report、matches、产品详情、PDF | 支持revisionId;报告默认最新成功版,指定版本必须属于该评估与本人 |
|
||||||
|
|
||||||
|
用户/会话锁和锁定当前读保护幂等与序号。任务只读取提交快照;迟到任务不能覆盖新版本。失败重试复用该输入版本,修改提交才创建新版本。
|
||||||
|
|
||||||
|
报告→匹配→详情→对比→助手→下一步行动→PDF始终传递同一revisionId。对比选择按用户、评估、版本隔离;登录白名单保留版本。历史单产品解释只读;新版发布时中止旧版未完成解释,迟到用量仍留原调用日志。助手新会话冻结版本facts/view,旧会话继续原上下文,不随重评切换。
|
||||||
|
|
||||||
|
## 验证和发布
|
||||||
|
|
||||||
|
MySQL5.7.26及MySQL8隔离库各51项集成检查通过,覆盖并发新建/修改、归属、幂等、分页、失败保留报告、历史匹配/PDF/助手。真实5.7建表、合成旧库迁移及备份恢复演练通过。浏览器通过本机真实控制器和数据库联调,模型为合成响应,不收费;不代表正式域名可用或真实模型语义验收。
|
||||||
|
|
||||||
|
H5最终使用现有Playwright CLI(Browser plugin not available)验证390×844/1280×900:页面标题/内容、无框架遮罩、历史报告→匹配→对比→助手版本一致、行动页→本人修改回填及离页重进通过,console/pageerror无新增错误。修改页离页提示改为v-show,避免卸载期间创建原生文本节点。页面截图在本机临时目录保留;合成UI用户、Token、队列已清理,临时8097/3309服务已停止。微信和Android为本地编译验证,未做真机及完整登录交互验收。
|
||||||
|
|
||||||
|
后端正式维护记录与恢复方案:`D:/AAdesk/202609/airz/database/assessment-revisions.md`。用户授权后已迁移111.230.144.231:3306/airz并部署21个源码文件,63项真实本机HTTPS/FPM及假模型检查通过,新建/修改/历史PDF/助手及清理均验证,0次收费调用;原助手与22张无关表保留,worker恢复并实际消费失效合成任务。外网HTTP现为301跳转HTTPS,HTTPS仍连接重置;不能将服务器本机TLS通过视为外网恢复。新版前端与后端已配套,发行体验版前需解决外网访问并做真机验证。
|
||||||
@@ -1,5 +1,21 @@
|
|||||||
# 融资需求与评估结果数据契约
|
# 融资需求与评估结果数据契约
|
||||||
|
|
||||||
|
## 2026-09-30 最新覆盖约定(评估版本)
|
||||||
|
|
||||||
|
最新存储/隐私/修改规则见[评估版本与最新资料](评估版本与最新资料.md):资料表只存每用户个人/企业各一条最新正式资料;新建增加会话,修改复用会话并新增日志JSON版本;匹配仍为会话match_result数组,历史结果和输入绑定revisionId。重评失败保留上次成功结果;身份业务资料明文,本人编辑完整回填,展示/PDF/模型上下文脱敏保留。当前代码已实施,正式迁移部署尚未执行;下述inputVersion=3采集范围继续有效,其余旧存储/只读/加密约定由本节覆盖。
|
||||||
|
|
||||||
|
## 2026-09-29 最新覆盖约定(inputVersion=3)
|
||||||
|
|
||||||
|
个人/企业信用融资恢复至2026-09-23 19:04需求扩展之前的采集范围:删除融资金额、融资期限、资金用途、其他用途说明;保留经营月数和原有身份/经营收入/资产负债/资料授权。提交使用inputVersion=3,结果仍为resultVersion=2,保留报告三栏、产品匹配、助手、PDF与行动入口。v3不生成需求事实或匹配规则,不把取消采集的字段列为“未提供/待核实”、补填项或完整度扣分项;产品自身额度、期限、利率和用途资料保留。其他真实缺失或来源未核验仍按既有规则处理。
|
||||||
|
|
||||||
|
资质匹配快照标记profileOnly=true并纳入摘要;编译产品规则时移除requestedAmountYuan、requestedTermMonths、purposeCode相关条件。新准备策略self-reported-profile-20260929-v2,个人11项/企业6项,不含需求条件项。旧输入1/2不再接受新建;保留旧报告解析分支仅供归档兼容。
|
||||||
|
|
||||||
|
已按用户授权在111.230.144.231/airz执行:删除报告161/229、关联会话151/219及2条日志;普通助手449及其2条日志保留,会员1个、产品6个保留。fa_user_info删除requested_amount、requested_term_months、purpose_code、purpose_detail四列;input_version、analysis_context属于必要版本和快照元数据,继续保留。两个触发器同步移除需求列校验,输入版本允许1/2/3,接口新建最低3。22张不相关表摘要一致,log_bin_trust_function_creators临时开启后恢复0。备份:D:/AAdesk/202609/airz-backups/restore-profile-20260929-180938/。
|
||||||
|
|
||||||
|
下文v2字段、用途字典、需求覆盖与旧保留历史约定为历史设计,不可据此恢复采集或重跑旧迁移;其他冻结、鉴权、幂等、三态、金额精度要求继续适用。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
版本:v1.6(匹配读接口及03/04/05/06基础已接,单产品AI解释与07—10扩展待开发)。日期:2026-09-24。配套:[开发文档](开发文档.md)、[开发进度](开发进度.md)。
|
版本:v1.6(匹配读接口及03/04/05/06基础已接,单产品AI解释与07—10扩展待开发)。日期:2026-09-24。配套:[开发文档](开发文档.md)、[开发进度](开发进度.md)。
|
||||||
|
|
||||||
本文补充融资需求字段、资料完整度与准备等级、产品匹配解释三个范围,作为后续前后端、数据迁移及验收的共同依据。依据为[开发说明书](开发说明书.docx)第2—5节及[原型图](原型.jpg)第02—05、09屏,并遵循用户后续确认的无草稿、H5优先、数据库存元及复用评估/报告表的约定。
|
本文补充融资需求字段、资料完整度与准备等级、产品匹配解释三个范围,作为后续前后端、数据迁移及验收的共同依据。依据为[开发说明书](开发说明书.docx)第2—5节及[原型图](原型.jpg)第02—05、09屏,并遵循用户后续确认的无草稿、H5优先、数据库存元及复用评估/报告表的约定。
|
||||||
|
|||||||
@@ -34,13 +34,13 @@
|
|||||||
onLoad((options) => {
|
onLoad((options) => {
|
||||||
consumeInitialLaunch()
|
consumeInitialLaunch()
|
||||||
if (options?.['resumeAssistant'] == '1') {
|
if (options?.['resumeAssistant'] == '1') {
|
||||||
const target = assistantReturnPath(options?.['assessmentId'] ?? '', options?.['productIds'] ?? '', options?.['chatId'] ?? '', options?.['from'] ?? '')
|
const target = assistantReturnPath(options?.['assessmentId'] ?? '', options?.['productIds'] ?? '', options?.['chatId'] ?? '', options?.['from'] ?? '', options?.['revisionId'] ?? '')
|
||||||
if (target.length > 0) afterLoginUrl = target
|
if (target.length > 0) afterLoginUrl = target
|
||||||
} else if (options?.['resumeNextStep'] == '1') {
|
} else if (options?.['resumeNextStep'] == '1') {
|
||||||
const target = nextStepRoute(options?.['assessmentId'] ?? '', options?.['productId'] ?? '', options?.['section'] ?? 'actions')
|
const target = nextStepRoute(options?.['assessmentId'] ?? '', options?.['productId'] ?? '', options?.['section'] ?? 'actions', options?.['revisionId'] ?? '')
|
||||||
if (target.length > 0) afterLoginUrl = target
|
if (target.length > 0) afterLoginUrl = target
|
||||||
} else if (options?.['resumeReport'] == '1') {
|
} else if (options?.['resumeReport'] == '1') {
|
||||||
const target = reportRoute(options?.['assessmentId'] ?? '', options?.['tab'] ?? 'overview')
|
const target = reportRoute(options?.['assessmentId'] ?? '', options?.['tab'] ?? 'overview', options?.['revisionId'] ?? '')
|
||||||
if (target.length > 0) afterLoginUrl = target
|
if (target.length > 0) afterLoginUrl = target
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
<text class="history-link">{{ historyOpen ? '收起记录' : '切换记录' }}</text>
|
<text class="history-link">{{ historyOpen ? '收起记录' : '切换记录' }}</text>
|
||||||
</view>
|
</view>
|
||||||
<view v-if="historyOpen" class="history-list">
|
<view v-if="historyOpen" class="history-list">
|
||||||
<text class="history-note">每次提交独立保存,请选择要查看的记录。</text>
|
<text class="history-note">新建评估独立保存;修改记录可在版本历史查看。</text>
|
||||||
<view v-for="item in records" :key="item.id" :class="item.id == selectedId ? 'history-row history-selected' : 'history-row'" @click="chooseRecord(item.id)">
|
<view v-for="item in records" :key="item.id" :class="item.id == selectedId ? 'history-row history-selected' : 'history-row'" @click="chooseRecord(item.id)">
|
||||||
<view class="record-caption"><text class="history-title">{{ item.submittedAt }} · #{{ item.id }}</text><text class="record-meta">{{ item.city }} · {{ item.statusLabel }}</text></view><text class="history-state">{{ item.id == selectedId ? '当前' : '查看' }}</text>
|
<view class="record-caption"><text class="history-title">{{ item.submittedAt }} · #{{ item.id }}</text><text class="record-meta">{{ item.city }} · {{ item.statusLabel }}</text></view><text class="history-state">{{ item.id == selectedId ? '当前' : '查看' }}</text>
|
||||||
</view>
|
</view>
|
||||||
@@ -50,10 +50,17 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
<view v-if="detail != null" class="record-picker">
|
||||||
|
<view class="record-picker-head"><text class="record-title">本评估版本历史</text></view>
|
||||||
|
<view v-for="item in versions" :key="item.revisionId" class="history-row" @click="chooseVersion(item.revisionId)"><text class="history-title">第 {{ item.revisionNo }} 版 · {{ item.submittedAt }} · {{ item.statusLabel }}</text><text class="history-state">{{ detail!.record.revisionId == item.revisionId ? '当前查看' : '查看' }}</text></view>
|
||||||
|
<text v-if="versionsError.length > 0" class="error-text">{{ versionsError }}</text><text v-if="versionsLoading" class="history-note">正在读取版本…</text><text v-else-if="versionsMore || versionsError.length > 0" class="text-action" @click="moreVersions">继续读取版本</text>
|
||||||
|
</view>
|
||||||
|
<button v-if="detail != null" class="new-action" @click="editAssessment">修改资料并重新评估</button>
|
||||||
<text v-if="detail != null && detail!.record.message.length > 0" class="task-note">{{ detail!.record.message }}</text>
|
<text v-if="detail != null && detail!.record.message.length > 0" class="task-note">{{ detail!.record.message }}</text>
|
||||||
<button v-if="detail != null" class="primary-button main-action" @click="openAnalysis">{{ detail!.record.status == 'succeeded' ? '查看分析报告' : '查看分析状态 / 重试' }}</button>
|
<button v-if="detail != null" class="primary-button main-action" @click="openAnalysis">{{ detail!.record.status == 'succeeded' ? '查看分析报告' : '查看分析状态 / 重试' }}</button>
|
||||||
|
<button v-if="detail != null && detail!.record.status != 'succeeded' && detail!.record.hasReport" class="new-action" @click="openPreviousReport">查看上次成功结果</button>
|
||||||
<button :class="detail == null ? 'primary-button main-action' : 'new-action'" :disabled="loading" @click="start">发起新的{{ type == 'personal' ? '个人' : '企业' }}评估</button>
|
<button :class="detail == null ? 'primary-button main-action' : 'new-action'" :disabled="loading" @click="start">发起新的{{ type == 'personal' ? '个人' : '企业' }}评估</button>
|
||||||
<text class="local-note">已提交资料只读保留;再次评估会新建记录。
|
<text class="local-note">修改会在本评估下保存新版本;新建评估另建记录。
|
||||||
未提交的填写内容不会保存。</text>
|
未提交的填写内容不会保存。</text>
|
||||||
</view>
|
</view>
|
||||||
</scroll-view>
|
</scroll-view>
|
||||||
@@ -65,16 +72,17 @@
|
|||||||
import { usePageLayout } from '@/services/layout.uts'
|
import { usePageLayout } from '@/services/layout.uts'
|
||||||
import { getSession } from '@/services/api.uts'
|
import { getSession } from '@/services/api.uts'
|
||||||
import { clearAssessmentFlow } from '@/services/assessment-flow.uts'
|
import { clearAssessmentFlow } from '@/services/assessment-flow.uts'
|
||||||
import { AssessmentRecord, AssessmentDetail, ArchiveSection, fetchAssessments, fetchAssessmentDetail, fetchArchiveCompleteness } from '@/packages/finance/services/records.uts'
|
import { AssessmentRecord, AssessmentDetail, ArchiveSection, fetchAssessments, fetchAssessmentDetail, fetchArchiveCompleteness, fetchAssessmentRevisions } from '@/packages/finance/services/records.uts'
|
||||||
const { pageStyle, contentStyle } = usePageLayout()
|
const { pageStyle, contentStyle } = usePageLayout()
|
||||||
const type = ref('personal'); const selectedId = ref(''); const records = ref<AssessmentRecord[]>([])
|
const type = ref('personal'); const selectedId = ref(''); const records = ref<AssessmentRecord[]>([])
|
||||||
|
const revisionId = ref(''); const versions = ref<AssessmentRecord[]>([]); const versionsMore = ref(false); const versionsError = ref(''); const versionsLoading = ref(false); let versionsCursor = ''
|
||||||
const detail = ref<AssessmentDetail | null>(null); const expanded = ref(''); const historyOpen = ref(false)
|
const detail = ref<AssessmentDetail | null>(null); const expanded = ref(''); const historyOpen = ref(false)
|
||||||
const loading = ref(false); const error = ref(''); const listError = ref(''); const listLoading = ref(false); const hasMore = ref(false)
|
const loading = ref(false); const error = ref(''); const listError = ref(''); const listLoading = ref(false); const hasMore = ref(false)
|
||||||
const completeness = ref<number | null>(null); const completenessFailed = ref(false)
|
const completeness = ref<number | null>(null); const completenessFailed = ref(false)
|
||||||
const completenessMessage = ref('提交评估后,查看对应报告的资料完整度。')
|
const completenessMessage = ref('提交评估后,查看对应报告的资料完整度。')
|
||||||
let cursor = ''; let generation = 0; let reportRequest = 0; let visible = false; let routeError = ''; let routeId = ''; let owner = ''
|
let cursor = ''; let generation = 0; let reportRequest = 0; let visible = false; let routeError = ''; let routeId = ''; let owner = ''
|
||||||
const sections = computed((): ArchiveSection[] => detail.value != null ? detail.value!.sections : [
|
const sections = computed((): ArchiveSection[] => detail.value != null ? detail.value!.sections : [
|
||||||
{ id: 'basic', icon: 'file-text', title: '基本信息', note: type.value == 'enterprise' ? '企业、联系人与融资需求' : '身份、联系方式与融资需求', fields: [], provided: 0 },
|
{ id: 'basic', icon: 'file-text', title: '基本信息', note: type.value == 'enterprise' ? '企业与联系人信息' : '身份与联系方式', fields: [], provided: 0 },
|
||||||
{ id: 'income', icon: 'income', title: type.value == 'enterprise' ? '经营信息' : '职业与收入', note: type.value == 'enterprise' ? '经营行业、月数与营收' : '职业、收入与缴纳情况', fields: [], provided: 0 },
|
{ id: 'income', icon: 'income', title: type.value == 'enterprise' ? '经营信息' : '职业与收入', note: type.value == 'enterprise' ? '经营行业、月数与营收' : '职业、收入与缴纳情况', fields: [], provided: 0 },
|
||||||
{ id: 'assets', icon: 'asset', title: '资产与负债', note: '贷款、信用卡额度与房车情况', fields: [], provided: 0 },
|
{ id: 'assets', icon: 'asset', title: '资产与负债', note: '贷款、信用卡额度与房车情况', fields: [], provided: 0 },
|
||||||
{ id: 'authorization', icon: 'authorization', title: '资料与授权', note: '私有资料与外部查询授权', fields: [], provided: 0 }
|
{ id: 'authorization', icon: 'authorization', title: '资料与授权', note: '私有资料与外部查询授权', fields: [], provided: 0 }
|
||||||
@@ -85,7 +93,7 @@
|
|||||||
return section.provided > 0 ? '已记录' : '未填写'
|
return section.provided > 0 ? '已记录' : '未填写'
|
||||||
}
|
}
|
||||||
const clearView = () => {
|
const clearView = () => {
|
||||||
detail.value = null; records.value = []; cursor = ''; hasMore.value = false; expanded.value = ''
|
detail.value = null; records.value = []; versions.value = []; versionsMore.value = false; versionsError.value = ''; versionsCursor = ''; versionsLoading.value = false; cursor = ''; hasMore.value = false; expanded.value = ''
|
||||||
completeness.value = null; completenessFailed.value = false; reportRequest++
|
completeness.value = null; completenessFailed.value = false; reportRequest++
|
||||||
completenessMessage.value = '提交评估后,查看对应报告的资料完整度。'
|
completenessMessage.value = '提交评估后,查看对应报告的资料完整度。'
|
||||||
error.value = ''; listError.value = ''; listLoading.value = false
|
error.value = ''; listError.value = ''; listLoading.value = false
|
||||||
@@ -104,7 +112,7 @@
|
|||||||
const reportSequence = ++reportRequest
|
const reportSequence = ++reportRequest
|
||||||
completeness.value = null; completenessFailed.value = false; completenessMessage.value = '正在读取本次报告的资料完整度…'
|
completeness.value = null; completenessFailed.value = false; completenessMessage.value = '正在读取本次报告的资料完整度…'
|
||||||
try {
|
try {
|
||||||
const result = await fetchArchiveCompleteness(id)
|
const result = await fetchArchiveCompleteness(id, detail.value?.record.revisionId ?? revisionId.value)
|
||||||
if (active(request, token) && reportSequence == reportRequest) { completeness.value = result.percent; completenessMessage.value = result.description }
|
if (active(request, token) && reportSequence == reportRequest) { completeness.value = result.percent; completenessMessage.value = result.description }
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (active(request, token) && reportSequence == reportRequest) {
|
if (active(request, token) && reportSequence == reportRequest) {
|
||||||
@@ -123,6 +131,18 @@
|
|||||||
if (active(request, token)) listError.value = err instanceof Error ? err.message : '历史记录读取失败,请重试'
|
if (active(request, token)) listError.value = err instanceof Error ? err.message : '历史记录读取失败,请重试'
|
||||||
} finally { if (active(request, token)) listLoading.value = false }
|
} finally { if (active(request, token)) listLoading.value = false }
|
||||||
}
|
}
|
||||||
|
const readVersions = async (append: boolean, request: number, token: string) => {
|
||||||
|
versionsLoading.value = true; versionsError.value = ''
|
||||||
|
try {
|
||||||
|
const page = await fetchAssessmentRevisions(selectedId.value, append ? versionsCursor : '')
|
||||||
|
if (!active(request, token)) return
|
||||||
|
versions.value = append ? versions.value.concat(page.items) : page.items; versionsMore.value = page.hasMore; versionsCursor = page.nextCursor
|
||||||
|
} catch (err) { if (active(request, token)) versionsError.value = err instanceof Error ? err.message : '版本历史读取失败' }
|
||||||
|
finally { if (active(request, token)) versionsLoading.value = false }
|
||||||
|
}
|
||||||
|
const moreVersions = () => { if (!versionsLoading.value) readVersions(versions.value.length > 0, generation, owner) }
|
||||||
|
const chooseVersion = (id: string) => { uni.redirectTo({ url: '/packages/finance/archive/archive?assessmentId=' + selectedId.value + '&revisionId=' + id }) }
|
||||||
|
const editAssessment = () => { if (detail.value != null) uni.navigateTo({ url: '/packages/finance/assessment/assessment?editId=' + selectedId.value }) }
|
||||||
const refresh = async () => {
|
const refresh = async () => {
|
||||||
const request = ++generation; clearView(); loading.value = true
|
const request = ++generation; clearView(); loading.value = true
|
||||||
const session = getSession()
|
const session = getSession()
|
||||||
@@ -132,7 +152,7 @@
|
|||||||
try {
|
try {
|
||||||
// 显式ID先校验本人详情;失败不能自动切到最新记录,掩盖越权或已删除状态。
|
// 显式ID先校验本人详情;失败不能自动切到最新记录,掩盖越权或已删除状态。
|
||||||
if (selectedId.value.length > 0) {
|
if (selectedId.value.length > 0) {
|
||||||
const result = await fetchAssessmentDetail(selectedId.value)
|
const result = await fetchAssessmentDetail(selectedId.value, revisionId.value)
|
||||||
if (!active(request, token)) return
|
if (!active(request, token)) return
|
||||||
detail.value = result; type.value = result.record.type
|
detail.value = result; type.value = result.record.type
|
||||||
await readList(false, request, token)
|
await readList(false, request, token)
|
||||||
@@ -142,7 +162,7 @@
|
|||||||
if (listError.value.length > 0) throw new Error(listError.value)
|
if (listError.value.length > 0) throw new Error(listError.value)
|
||||||
if (records.value.length > 0) {
|
if (records.value.length > 0) {
|
||||||
selectedId.value = records.value[0].id
|
selectedId.value = records.value[0].id
|
||||||
const result = await fetchAssessmentDetail(selectedId.value)
|
const result = await fetchAssessmentDetail(selectedId.value, revisionId.value)
|
||||||
if (!active(request, token)) return
|
if (!active(request, token)) return
|
||||||
detail.value = result
|
detail.value = result
|
||||||
}
|
}
|
||||||
@@ -151,6 +171,7 @@
|
|||||||
if (listError.value.length > 0) historyOpen.value = true
|
if (listError.value.length > 0) historyOpen.value = true
|
||||||
loading.value = false
|
loading.value = false
|
||||||
if (detail.value != null) {
|
if (detail.value != null) {
|
||||||
|
await readVersions(false, request, token)
|
||||||
if (detail.value!.record.status == 'succeeded') await readCompleteness(detail.value!.record.id, request, token)
|
if (detail.value!.record.status == 'succeeded') await readCompleteness(detail.value!.record.id, request, token)
|
||||||
else completenessMessage.value = detail.value!.record.status == 'failed' ? '本次分析未完成,暂没有报告完整度;已提交资料已保留。' : '本次分析尚未完成,报告生成后可查看完整度。'
|
else completenessMessage.value = detail.value!.record.status == 'failed' ? '本次分析未完成,暂没有报告完整度;已提交资料已保留。' : '本次分析尚未完成,报告生成后可查看完整度。'
|
||||||
}
|
}
|
||||||
@@ -167,12 +188,15 @@
|
|||||||
const chooseRecord = (id: string) => { if (id == selectedId.value) { historyOpen.value = false; return }; uni.redirectTo({ url: '/packages/finance/archive/archive?type=' + type.value + '&assessmentId=' + encodeURIComponent(id) }) }
|
const chooseRecord = (id: string) => { if (id == selectedId.value) { historyOpen.value = false; return }; uni.redirectTo({ url: '/packages/finance/archive/archive?type=' + type.value + '&assessmentId=' + encodeURIComponent(id) }) }
|
||||||
const showLatest = () => { uni.redirectTo({ url: '/packages/finance/archive/archive?type=' + type.value }) }
|
const showLatest = () => { uni.redirectTo({ url: '/packages/finance/archive/archive?type=' + type.value }) }
|
||||||
const start = () => { clearAssessmentFlow(); uni.navigateTo({ url: '/packages/finance/assessment/assessment?type=' + type.value }) }
|
const start = () => { clearAssessmentFlow(); uni.navigateTo({ url: '/packages/finance/assessment/assessment?type=' + type.value }) }
|
||||||
|
const openPreviousReport = () => { if (detail.value != null) uni.navigateTo({ url: '/packages/finance/report/report?assessmentId=' + selectedId.value + '&revisionId=' + detail.value!.record.publishedRevisionId }) }
|
||||||
const openAnalysis = () => {
|
const openAnalysis = () => {
|
||||||
if (detail.value == null) return
|
if (detail.value == null) return
|
||||||
const page = detail.value!.record.status == 'succeeded' ? 'report/report' : 'analyzing/analyzing'
|
const page = detail.value!.record.status == 'succeeded' ? 'report/report' : 'analyzing/analyzing'
|
||||||
uni.navigateTo({ url: '/packages/finance/' + page + '?assessmentId=' + encodeURIComponent(detail.value!.record.id) })
|
uni.navigateTo({ url: '/packages/finance/' + page + '?assessmentId=' + encodeURIComponent(detail.value!.record.id) + (page == 'report/report' ? '&revisionId=' + detail.value!.record.revisionId : '') })
|
||||||
}
|
}
|
||||||
onLoad((options) => {
|
onLoad((options) => {
|
||||||
|
revisionId.value = options?.['revisionId'] ?? ''
|
||||||
|
if (revisionId.value.length > 0 && !/^[1-9][0-9]{0,9}$/.test(revisionId.value)) routeError = '版本编号无效,请重新选择。'
|
||||||
type.value = options?.['type'] ?? 'personal'; routeId = options?.['assessmentId'] ?? ''; selectedId.value = routeId
|
type.value = options?.['type'] ?? 'personal'; routeId = options?.['assessmentId'] ?? ''; selectedId.value = routeId
|
||||||
if (!['personal', 'enterprise'].includes(type.value)) { type.value = 'personal'; routeError = '档案类型无效,请重新选择。' }
|
if (!['personal', 'enterprise'].includes(type.value)) { type.value = 'personal'; routeError = '档案类型无效,请重新选择。' }
|
||||||
if (options?.['assessmentId'] != null && !/^[1-9][0-9]{0,9}$/.test(routeId)) routeError = '评估编号无效,请重新选择历史记录。'
|
if (options?.['assessmentId'] != null && !/^[1-9][0-9]{0,9}$/.test(routeId)) routeError = '评估编号无效,请重新选择历史记录。'
|
||||||
|
|||||||
@@ -3,7 +3,8 @@
|
|||||||
<AppHeader :title="titles[step]" :customBack="true" @back="back" />
|
<AppHeader :title="titles[step]" :customBack="true" @back="back" />
|
||||||
<scroll-view scroll-y :style="contentStyle(96, false)">
|
<scroll-view scroll-y :style="contentStyle(96, false)">
|
||||||
<view class="assessment-content">
|
<view class="assessment-content">
|
||||||
<text class="local-note">未提交内容不会保存,离开或刷新后需重新填写。</text><view class="progress-row"><view class="step-line"><view v-for="i in 5" :key="i" :class="i <= step + 1 ? 'step-segment done' : 'step-segment'"></view></view><text class="step-count">{{ step + 1 }}/5</text></view>
|
<!-- 离页清空表单时仅切换提示可见性,避免卸载期间创建原生文本节点。 -->
|
||||||
|
<text v-if="editId.length > 0" class="section-hint">修改本次评估 · 提交后保留此前版本</text><text v-show="editLoading" class="local-note">正在读取已提交资料…</text><text v-show="editError.length > 0" class="local-note">{{ editError }}</text><text class="local-note">未提交内容不会保存,离开或刷新后需重新填写。</text><view class="progress-row"><view class="step-line"><view v-for="i in 5" :key="i" :class="i <= step + 1 ? 'step-segment done' : 'step-segment'"></view></view><text class="step-count">{{ step + 1 }}/5</text></view>
|
||||||
<view v-if="step == 0" class="type-row"><text class="type-label">融资类型</text><view class="type-switch"><text :class="assessmentType == 'personal' ? 'type-item type-active' : 'type-item'" @click="switchType('personal')">个人</text><text :class="assessmentType == 'enterprise' ? 'type-item type-active' : 'type-item'" @click="switchType('enterprise')">企业</text></view></view>
|
<view v-if="step == 0" class="type-row"><text class="type-label">融资类型</text><view class="type-switch"><text :class="assessmentType == 'personal' ? 'type-item type-active' : 'type-item'" @click="switchType('personal')">个人</text><text :class="assessmentType == 'enterprise' ? 'type-item type-active' : 'type-item'" @click="switchType('enterprise')">企业</text></view></view>
|
||||||
<text v-else class="section-hint">{{ assessmentType == 'enterprise' ? '企业信用融资' : '个人信用融资' }} · {{ step == 3 ? '资料与授权均为选填' : '请按实际情况填写' }}</text>
|
<text v-else class="section-hint">{{ assessmentType == 'enterprise' ? '企业信用融资' : '个人信用融资' }} · {{ step == 3 ? '资料与授权均为选填' : '请按实际情况填写' }}</text>
|
||||||
<view v-if="step == 0" class="form-list">
|
<view v-if="step == 0" class="form-list">
|
||||||
@@ -29,14 +30,7 @@
|
|||||||
<AssessmentSelect label="社保缴纳" v-model="form.socialSecurity" :options="['连续缴纳 6 个月以上', '缴纳不足 6 个月', '未缴纳']" />
|
<AssessmentSelect label="社保缴纳" v-model="form.socialSecurity" :options="['连续缴纳 6 个月以上', '缴纳不足 6 个月', '未缴纳']" />
|
||||||
<AssessmentSelect label="公积金缴纳" v-model="form.housingFund" :options="['连续缴纳 6 个月以上', '缴纳不足 6 个月', '未缴纳']" />
|
<AssessmentSelect label="公积金缴纳" v-model="form.housingFund" :options="['连续缴纳 6 个月以上', '缴纳不足 6 个月', '未缴纳']" />
|
||||||
</template>
|
</template>
|
||||||
<text class="asset-section-title">资金需求(必填)</text>
|
|
||||||
<text class="local-note">填写您期望的金额、期限和用途,不代表机构审批结果。</text>
|
|
||||||
<view class="form-row"><text class="field-label">融资金额</text><input v-model="form.requestedAmountWan" class="field-input" type="digit" maxlength="15" placeholder="请输入期望金额" /><text class="field-unit">万元</text></view>
|
|
||||||
<view class="form-row"><text class="field-label">融资期限</text><input v-model="form.requestedTermMonths" class="field-input" type="number" maxlength="4" placeholder="请输入整数月" /><text class="field-unit">个月</text></view>
|
|
||||||
<text v-if="purposesLoading" class="local-note">正在加载资金用途…</text>
|
|
||||||
<view v-else-if="purposeError.length > 0" class="purpose-error"><text class="local-note">{{ purposeError }}</text><button class="purpose-retry" @click="loadPurposes">重新加载用途</button></view>
|
|
||||||
<AssessmentSelect v-else label="资金用途" :modelValue="form.purposeLabel" @update:modelValue="purposeChanged" :options="purposeLabels" />
|
|
||||||
<view v-if="form.purposeCode == 'other'" class="purpose-detail"><text class="field-label">用途说明</text><textarea v-model="form.purposeDetail" class="purpose-textarea" maxlength="200" placeholder="请简述资金用途(2至200字)" /></view>
|
|
||||||
</view>
|
</view>
|
||||||
<view v-else-if="step == 2" class="form-list">
|
<view v-else-if="step == 2" class="form-list">
|
||||||
<text class="asset-section-title">贷款信息</text>
|
<text class="asset-section-title">贷款信息</text>
|
||||||
@@ -61,15 +55,16 @@
|
|||||||
<view class="scroll-spacer" :style="bottomStyle(24)"></view>
|
<view class="scroll-spacer" :style="bottomStyle(24)"></view>
|
||||||
</view>
|
</view>
|
||||||
</scroll-view>
|
</scroll-view>
|
||||||
<view class="bottom-action" :style="bottomStyle(20)"><button v-if="step > 0" class="back-button" @click="previous">上一步</button><button class="primary-button next-button" @click="next">{{ step == 3 ? '确认信息' : '下一步' }}</button></view>
|
<view class="bottom-action" :style="bottomStyle(20)"><button v-if="step > 0" class="back-button" @click="previous">上一步</button><button class="primary-button next-button" :disabled="editLoading || editError.length > 0" @click="next">{{ step == 3 ? '确认信息' : '下一步' }}</button></view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="uts">
|
<script setup lang="uts">
|
||||||
import { getSession } from '@/services/api.uts'
|
import { getSession } from '@/services/api.uts'
|
||||||
|
|
||||||
|
import { fetchAssessmentEdit } from '@/packages/finance/services/records.uts'
|
||||||
|
import { writeAssessmentFlow } from '@/services/assessment-flow.uts'
|
||||||
import { wanAmountValid } from '@/services/money.uts'
|
import { wanAmountValid } from '@/services/money.uts'
|
||||||
import { FinancingPurpose, fetchFinancingPurposes, demandValidationMessage } from '@/services/assessment-demand.uts'
|
|
||||||
import { usePageLayout } from '@/services/layout.uts'
|
import { usePageLayout } from '@/services/layout.uts'
|
||||||
import AppHeader from '@/components/AppHeader.uvue'
|
import AppHeader from '@/components/AppHeader.uvue'
|
||||||
import AppIcon from '@/components/AppIcon.uvue'
|
import AppIcon from '@/components/AppIcon.uvue'
|
||||||
@@ -77,47 +72,28 @@
|
|||||||
import { AssessmentForm, SelectedDocument, DataAuthorization, emptyAssessmentForm, activeAssessmentType, getAssessmentInput, formFromInput, documentsFromInput, setAssessmentInput, showAgreementNotice, assetValidationMessage, clearAssessmentFlow } from '@/services/assessment.uts'
|
import { AssessmentForm, SelectedDocument, DataAuthorization, emptyAssessmentForm, activeAssessmentType, getAssessmentInput, formFromInput, documentsFromInput, setAssessmentInput, showAgreementNotice, assetValidationMessage, clearAssessmentFlow } from '@/services/assessment.uts'
|
||||||
const { bottomStyle, contentStyle, pageStyle } = usePageLayout()
|
const { bottomStyle, contentStyle, pageStyle } = usePageLayout()
|
||||||
const showAgreement = (title: string) => { showAgreementNotice(title) }
|
const showAgreement = (title: string) => { showAgreementNotice(title) }
|
||||||
|
const editId = ref(''); const editError = ref(''); const editLoading = ref(false)
|
||||||
|
let baseRevisionId = ''; let editGeneration = 0
|
||||||
const step = ref(0)
|
const step = ref(0)
|
||||||
const assessmentType = ref('personal')
|
const assessmentType = ref('personal')
|
||||||
const form = ref<AssessmentForm>(emptyAssessmentForm())
|
const form = ref<AssessmentForm>(emptyAssessmentForm())
|
||||||
let ownerToken = ''; let goingToConfirm = false
|
let ownerToken = ''; let goingToConfirm = false
|
||||||
const sameOwner = (): boolean => { const session = getSession(); return session != null && session['token'] == ownerToken }
|
const sameOwner = (): boolean => { const session = getSession(); return session != null && session['token'] == ownerToken }
|
||||||
const purposes = ref<FinancingPurpose[]>([])
|
|
||||||
const purposesLoading = ref(false)
|
|
||||||
const purposeError = ref('')
|
|
||||||
const purposeLabels = computed((): string[] => purposes.value.filter((item) => item.type == assessmentType.value).map((item) => item.label))
|
|
||||||
const purposeChanged = (label: string) => {
|
|
||||||
const item = purposes.value.find((entry) => entry.type == assessmentType.value && entry.label == label)
|
|
||||||
if (item == null) return
|
|
||||||
form.value.purposeCode = item.code; form.value.purposeLabel = item.label
|
|
||||||
// 从其他用途切走时清空说明,避免提交隐藏的过期内容。
|
|
||||||
if (!item.requiresDetail) form.value.purposeDetail = ''
|
|
||||||
}
|
|
||||||
const loadPurposes = async () => {
|
|
||||||
if (purposesLoading.value) return
|
|
||||||
purposesLoading.value = true; purposeError.value = ''
|
|
||||||
try {
|
|
||||||
const items = await fetchFinancingPurposes()
|
|
||||||
if (sameOwner()) purposes.value = items
|
|
||||||
} catch (error) {
|
|
||||||
if (sameOwner()) purposeError.value = error instanceof Error ? error.message : '资金用途加载失败,请重试'
|
|
||||||
} finally { purposesLoading.value = false }
|
|
||||||
}
|
|
||||||
const customCity = ref('')
|
const customCity = ref('')
|
||||||
const cityChanged = (city: string) => { form.value.city = city; if (city != '其他城市') customCity.value = '' }
|
const cityChanged = (city: string) => { form.value.city = city; if (city != '其他城市') customCity.value = '' }
|
||||||
const documentConsent = ref(false)
|
const documentConsent = ref(false)
|
||||||
const dataConsent = ref(false)
|
const dataConsent = ref(false)
|
||||||
const documents = ref<SelectedDocument[]>([])
|
const documents = ref<SelectedDocument[]>([])
|
||||||
const authorizations = ref<DataAuthorization[]>([{ key: 'credit', name: '征信查询', selected: false }, { key: 'business', name: '经营数据', selected: false }, { key: 'fund', name: '公积金数据', selected: false }])
|
const authorizations = ref<DataAuthorization[]>([{ key: 'credit', name: '征信查询', selected: false }, { key: 'business', name: '经营数据', selected: false }, { key: 'fund', name: '公积金数据', selected: false }])
|
||||||
const titles = ['基本信息', '收入与资金需求', '资产 / 负债信息', '资料上传 / 授权']
|
const titles = ['基本信息', '经营 / 收入信息', '资产 / 负债信息', '资料上传 / 授权']
|
||||||
const uploadNames = computed((): string[] => assessmentType.value == 'enterprise' ? ['营业执照', '银行流水', '经营证明'] : ['身份证正面', '身份证反面', '银行卡', '收入证明'])
|
const uploadNames = computed((): string[] => assessmentType.value == 'enterprise' ? ['营业执照', '银行流水', '经营证明'] : ['身份证正面', '身份证反面', '银行卡', '收入证明'])
|
||||||
let initialized = false
|
let initialized = false
|
||||||
const rememberInput = () => {
|
const rememberInput = () => {
|
||||||
if (!initialized || !sameOwner()) return
|
if (!initialized || !sameOwner()) return
|
||||||
const source = form.value
|
const source = form.value
|
||||||
const currentForm: AssessmentForm = { name: source.name, idCard: source.idCard, phone: source.phone, city: source.city, maritalStatus: source.maritalStatus, occupation: source.occupation, company: source.company, monthlyIncome: source.monthlyIncome, socialSecurity: source.socialSecurity, housingFund: source.housingFund, house: source.house, car: source.car, savings: source.savings, creditCard: source.creditCard, otherAssets: source.otherAssets, creditLoanCount: source.creditLoanCount, totalDebtBalance: source.totalDebtBalance, monthlyLoanPayment: source.monthlyLoanPayment, creditCardCount: source.creditCardCount, creditCardTotalLimit: source.creditCardTotalLimit, creditCardUsedLimit: source.creditCardUsedLimit, enterpriseName: source.enterpriseName, businessCode: source.businessCode, annualRevenue: source.annualRevenue, operatingYears: source.operatingYears, operatingMonths: source.operatingMonths, requestedAmountWan: source.requestedAmountWan, requestedTermMonths: source.requestedTermMonths, purposeCode: source.purposeCode, purposeLabel: source.purposeLabel, purposeDetail: source.purposeDetail }
|
const currentForm: AssessmentForm = { name: source.name, idCard: source.idCard, phone: source.phone, city: source.city, maritalStatus: source.maritalStatus, occupation: source.occupation, company: source.company, monthlyIncome: source.monthlyIncome, socialSecurity: source.socialSecurity, housingFund: source.housingFund, house: source.house, car: source.car, savings: source.savings, creditCard: source.creditCard, otherAssets: source.otherAssets, creditLoanCount: source.creditLoanCount, totalDebtBalance: source.totalDebtBalance, monthlyLoanPayment: source.monthlyLoanPayment, creditCardCount: source.creditCardCount, creditCardTotalLimit: source.creditCardTotalLimit, creditCardUsedLimit: source.creditCardUsedLimit, enterpriseName: source.enterpriseName, businessCode: source.businessCode, annualRevenue: source.annualRevenue, operatingYears: source.operatingYears, operatingMonths: source.operatingMonths }
|
||||||
if (currentForm.city == '其他城市' && customCity.value.trim().length > 0) currentForm.city = customCity.value.trim()
|
if (currentForm.city == '其他城市' && customCity.value.trim().length > 0) currentForm.city = customCity.value.trim()
|
||||||
setAssessmentInput(assessmentType.value, step.value, currentForm, documents.value, authorizations.value, documentConsent.value, dataConsent.value)
|
setAssessmentInput(assessmentType.value, step.value, currentForm, documents.value, authorizations.value, documentConsent.value, dataConsent.value, editId.value, baseRevisionId)
|
||||||
}
|
}
|
||||||
const restore = (type: string) => {
|
const restore = (type: string) => {
|
||||||
assessmentType.value = type; step.value = 0; form.value = emptyAssessmentForm(); customCity.value = ''; documents.value = []; documentConsent.value = false; dataConsent.value = false
|
assessmentType.value = type; step.value = 0; form.value = emptyAssessmentForm(); customCity.value = ''; documents.value = []; documentConsent.value = false; dataConsent.value = false
|
||||||
@@ -134,7 +110,17 @@
|
|||||||
initialized = true
|
initialized = true
|
||||||
rememberInput()
|
rememberInput()
|
||||||
}
|
}
|
||||||
const switchType = (type: string) => { if (assessmentType.value == type) return; rememberInput(); restore(type); step.value = 0; rememberInput() }
|
const switchType = (type: string) => { if (editId.value.length > 0 || assessmentType.value == type) return; rememberInput(); restore(type); step.value = 0; rememberInput() }
|
||||||
|
const loadEdit = async (type: string) => {
|
||||||
|
const ticket = ++editGeneration; editLoading.value = true
|
||||||
|
try {
|
||||||
|
const saved = await fetchAssessmentEdit(editId.value)
|
||||||
|
if (ticket != editGeneration || !sameOwner()) return
|
||||||
|
baseRevisionId = saved.getString('baseRevisionId') ?? ''
|
||||||
|
writeAssessmentFlow(ownerToken, saved); restore(saved.getString('type') ?? type)
|
||||||
|
} catch (err) { if (ticket == editGeneration && sameOwner()) editError.value = err instanceof Error ? err.message : '资料读取失败,请返回重试。' }
|
||||||
|
finally { if (ticket == editGeneration) editLoading.value = false }
|
||||||
|
}
|
||||||
onLoad((options) => {
|
onLoad((options) => {
|
||||||
const session = getSession()
|
const session = getSession()
|
||||||
if (session == null) { uni.redirectTo({ url: '/packages/account/login/login' }); return }
|
if (session == null) { uni.redirectTo({ url: '/packages/account/login/login' }); return }
|
||||||
@@ -142,8 +128,10 @@
|
|||||||
clearAssessmentFlow()
|
clearAssessmentFlow()
|
||||||
let type = activeAssessmentType()
|
let type = activeAssessmentType()
|
||||||
if (options != null && (options['type'] == 'enterprise' || options['type'] == 'personal')) type = options['type'] as string
|
if (options != null && (options['type'] == 'enterprise' || options['type'] == 'personal')) type = options['type'] as string
|
||||||
restore(type)
|
editId.value = options?.['editId'] ?? ''
|
||||||
loadPurposes()
|
if (editId.value.length == 0) { restore(type); return }
|
||||||
|
if (!/^[1-9][0-9]{0,9}$/.test(editId.value)) { editError.value = '评估编号无效,请返回档案重新选择。'; return }
|
||||||
|
loadEdit(type)
|
||||||
})
|
})
|
||||||
onShow(() => {
|
onShow(() => {
|
||||||
goingToConfirm = false
|
goingToConfirm = false
|
||||||
@@ -154,12 +142,14 @@
|
|||||||
// 离开填写流程即丢弃未提交内容;进入确认页时只在内存传递。
|
// 离开填写流程即丢弃未提交内容;进入确认页时只在内存传递。
|
||||||
onHide(() => {
|
onHide(() => {
|
||||||
if (!goingToConfirm) {
|
if (!goingToConfirm) {
|
||||||
clearAssessmentFlow(); form.value = emptyAssessmentForm(); step.value = 0; documents.value = []
|
editGeneration++; if (editId.value.length > 0) editError.value = '已离开修改流程,请返回档案重新进入。'
|
||||||
|
// 离页清空资料但不切换步骤,避免卸载时又挂载另一组原生输入组件。
|
||||||
|
clearAssessmentFlow(); form.value = emptyAssessmentForm(); documents.value = []
|
||||||
customCity.value = ''; documentConsent.value = false; dataConsent.value = false
|
customCity.value = ''; documentConsent.value = false; dataConsent.value = false
|
||||||
authorizations.value.forEach((item) => { item.selected = false })
|
authorizations.value.forEach((item) => { item.selected = false })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
onUnload(() => { initialized = false; clearAssessmentFlow() })
|
onUnload(() => { editGeneration++; initialized = false; clearAssessmentFlow() })
|
||||||
// 页内前后步骤只维护当前内存状态,返回入口后不保留填写内容。
|
// 页内前后步骤只维护当前内存状态,返回入口后不保留填写内容。
|
||||||
onBackPress((): boolean => { rememberInput(); if (step.value > 0) { step.value--; rememberInput(); return true }; return false })
|
onBackPress((): boolean => { rememberInput(); if (step.value > 0) { step.value--; rememberInput(); return true }; return false })
|
||||||
const previous = () => { rememberInput(); if (step.value > 0) step.value--; rememberInput() }
|
const previous = () => { rememberInput(); if (step.value > 0) step.value--; rememberInput() }
|
||||||
@@ -199,9 +189,6 @@
|
|||||||
if (step.value == 1) {
|
if (step.value == 1) {
|
||||||
const amount = assessmentType.value == 'enterprise' ? form.value.annualRevenue : form.value.monthlyIncome
|
const amount = assessmentType.value == 'enterprise' ? form.value.annualRevenue : form.value.monthlyIncome
|
||||||
if (!wanAmountValid(amount) || parseFloat(amount) <= 0) message = assessmentType.value == 'enterprise' ? '请输入有效年营收(万元,最多六位小数)' : '请输入有效月收入(万元,最多六位小数)'
|
if (!wanAmountValid(amount) || parseFloat(amount) <= 0) message = assessmentType.value == 'enterprise' ? '请输入有效年营收(万元,最多六位小数)' : '请输入有效月收入(万元,最多六位小数)'
|
||||||
else if (purposesLoading.value || purposeError.value.length > 0) message = '请先加载资金用途后继续'
|
|
||||||
else if (!purposes.value.some((item) => item.type == assessmentType.value && item.code == form.value.purposeCode)) message = '请选择资金用途'
|
|
||||||
else message = demandValidationMessage(form.value.requestedAmountWan, form.value.requestedTermMonths, form.value.purposeCode, form.value.purposeDetail)
|
|
||||||
}
|
}
|
||||||
if (step.value == 2) message = assetValidationMessage(form.value)
|
if (step.value == 2) message = assetValidationMessage(form.value)
|
||||||
if (step.value == 3 && documents.value.length > 0 && !documentConsent.value) message = '请阅读并同意资料使用协议,或移除资料'
|
if (step.value == 3 && documents.value.length > 0 && !documentConsent.value) message = '请阅读并同意资料使用协议,或移除资料'
|
||||||
@@ -209,7 +196,7 @@
|
|||||||
if (message.length == 0) return true
|
if (message.length == 0) return true
|
||||||
uni.showToast({ title: message, icon: 'none' }); return false
|
uni.showToast({ title: message, icon: 'none' }); return false
|
||||||
}
|
}
|
||||||
const next = () => { if (!validateStep()) return; if (step.value < 3) { step.value++; rememberInput() } else { rememberInput(); goingToConfirm = true; uni.navigateTo({ url: '/packages/finance/confirm/confirm', fail: () => { goingToConfirm = false } }) } }
|
const next = () => { if (editLoading.value || editError.value.length > 0 || !sameOwner()) return; if (!validateStep()) return; if (step.value < 3) { step.value++; rememberInput() } else { rememberInput(); goingToConfirm = true; uni.navigateTo({ url: '/packages/finance/confirm/confirm', fail: () => { goingToConfirm = false } }) } }
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@@ -231,9 +218,6 @@
|
|||||||
.form-row { min-height: 106rpx; flex-direction: row; align-items: center; border-bottom: 1rpx solid #edf1f6; }
|
.form-row { min-height: 106rpx; flex-direction: row; align-items: center; border-bottom: 1rpx solid #edf1f6; }
|
||||||
.field-label { font-size: 28rpx; color: #29374e; margin-right: 18rpx; flex-shrink: 0; }
|
.field-label { font-size: 28rpx; color: #29374e; margin-right: 18rpx; flex-shrink: 0; }
|
||||||
.field-input { flex: 1; min-width: 0; text-align: right; font-size: 27rpx; color: #22324a; }
|
.field-input { flex: 1; min-width: 0; text-align: right; font-size: 27rpx; color: #22324a; }
|
||||||
.purpose-detail { margin-top: 26rpx; }
|
|
||||||
.purpose-textarea { width: 100%; height: 150rpx; margin-top: 16rpx; padding: 18rpx; border-radius: 12rpx; background-color: #f5f7fb; font-size: 26rpx; color: #22324a; }
|
|
||||||
.purpose-retry { margin: 14rpx 0; font-size: 24rpx; color: #0868f7; background-color: #eff6ff; }
|
|
||||||
.upload-title { font-size: 30rpx; font-weight: 600; color: #20304a; margin-top: 28rpx; }
|
.upload-title { font-size: 30rpx; font-weight: 600; color: #20304a; margin-top: 28rpx; }
|
||||||
.optional { font-size: 24rpx; color: #748198; font-weight: 400; }
|
.optional { font-size: 24rpx; color: #748198; font-weight: 400; }
|
||||||
.upload-desc { font-size: 24rpx; line-height: 36rpx; color: #748198; margin-top: 10rpx; }
|
.upload-desc { font-size: 24rpx; line-height: 36rpx; color: #748198; margin-top: 10rpx; }
|
||||||
|
|||||||
@@ -20,7 +20,6 @@
|
|||||||
<template v-if="contextOpen">
|
<template v-if="contextOpen">
|
||||||
<text v-if="context!.products.length == 0" class="context-note">围绕这份已保存报告解答,不重新评估</text>
|
<text v-if="context!.products.length == 0" class="context-note">围绕这份已保存报告解答,不重新评估</text>
|
||||||
<view v-for="item in context!.products" :key="item.id" class="context-product"><text class="context-product-name">{{ item.name }}</text><text :class="item.state == 'eligible' ? 'product-state good-state' : 'product-state'">{{ item.stateLabel }}</text></view>
|
<view v-for="item in context!.products" :key="item.id" class="context-product"><text class="context-product-name">{{ item.name }}</text><text :class="item.state == 'eligible' ? 'product-state good-state' : 'product-state'">{{ item.stateLabel }}</text></view>
|
||||||
<text v-if="hasDemo" class="context-note">演示产品 · 仅供功能验证</text>
|
|
||||||
<text class="context-link context-source" @click="source">{{ sourceLabel }}</text>
|
<text class="context-link context-source" @click="source">{{ sourceLabel }}</text>
|
||||||
</template>
|
</template>
|
||||||
</view>
|
</view>
|
||||||
@@ -50,7 +49,7 @@
|
|||||||
import { parseCompareIds } from '@/packages/finance/services/compare.uts'
|
import { parseCompareIds } from '@/packages/finance/services/compare.uts'
|
||||||
import { AssistantProduct, AssistantContext, AssistantTurn, AssistantHistory, assistantKey, assistantRoute, previewAssistant, loadAssistant, createAssistant, assistantMessages, submitAssistant, assistantHistory } from '@/packages/finance/services/assistant.uts'
|
import { AssistantProduct, AssistantContext, AssistantTurn, AssistantHistory, assistantKey, assistantRoute, previewAssistant, loadAssistant, createAssistant, assistantMessages, submitAssistant, assistantHistory } from '@/packages/finance/services/assistant.uts'
|
||||||
const { pageStyle, contentStyle, bottomStyle } = usePageLayout()
|
const { pageStyle, contentStyle, bottomStyle } = usePageLayout()
|
||||||
const assessmentId = ref(''); const productIds = ref<string[]>([]); const chatId = ref(''); const from = ref('')
|
const revisionId = ref(''); const assessmentId = ref(''); const productIds = ref<string[]>([]); const chatId = ref(''); const from = ref('')
|
||||||
const context = ref<AssistantContext | null>(null); const messages = ref<AssistantTurn[]>([])
|
const context = ref<AssistantContext | null>(null); const messages = ref<AssistantTurn[]>([])
|
||||||
// 展开只影响展示;切换会话或账户时恢复折叠,不改变已绑定的报告和产品。
|
// 展开只影响展示;切换会话或账户时恢复折叠,不改变已绑定的报告和产品。
|
||||||
const contextOpen = ref(false)
|
const contextOpen = ref(false)
|
||||||
@@ -62,21 +61,20 @@
|
|||||||
let historyCursor = ''; let createKey = assistantKey(); let bindingSpecified = false; let generation = 0; let scope = ''; let timer = 0; let polling = false; let pollCount = 0
|
let historyCursor = ''; let createKey = assistantKey(); let bindingSpecified = false; let generation = 0; let scope = ''; let timer = 0; let polling = false; let pollCount = 0
|
||||||
let sendKey = ''; let sendText = ''; let sendAttempt = 0
|
let sendKey = ''; let sendText = ''; let sendAttempt = 0
|
||||||
const pending = computed((): boolean => messages.value.some((t: AssistantTurn): boolean => t.status == 'queued' || t.status == 'running'))
|
const pending = computed((): boolean => messages.value.some((t: AssistantTurn): boolean => t.status == 'queued' || t.status == 'running'))
|
||||||
const hasDemo = computed((): boolean => context.value?.products.some((p: AssistantProduct): boolean => p.demo) ?? false)
|
|
||||||
const lastRequestId = computed((): string => messages.value.length > 0 ? messages.value[messages.value.length - 1].requestId : '')
|
const lastRequestId = computed((): string => messages.value.length > 0 ? messages.value[messages.value.length - 1].requestId : '')
|
||||||
const canSend = computed((): boolean => context.value != null && !sending.value && !pending.value && !uncertain.value && draft.value.trim().length > 0)
|
const canSend = computed((): boolean => context.value != null && !sending.value && !pending.value && !uncertain.value && draft.value.trim().length > 0)
|
||||||
const sourceLabel = computed((): string => from.value == 'next-step' ? '返回下一步行动 ›' : (from.value == 'product' && productIds.value.length == 1 ? '返回产品 ›' : (from.value == 'compare' ? '返回对比 ›' : '查看报告 ›')))
|
const sourceLabel = computed((): string => from.value == 'next-step' ? '返回下一步行动 ›' : (from.value == 'product' && productIds.value.length == 1 ? '返回产品 ›' : (from.value == 'compare' ? '返回对比 ›' : '查看报告 ›')))
|
||||||
const statusLabel = (item: AssistantTurn): string => item.status == 'queued' ? '问题已收到,等待AI回复…' : (item.status == 'running' ? '正在整理建议…' : (item.status == 'local' ? (sending.value ? '正在确认发送结果…' : '尚未确认发送结果,请查看下方提示。') : '回复暂未完成。' + (item.canRetry ? '您可以重新尝试。' : '已达到本条消息重试上限。')))
|
const statusLabel = (item: AssistantTurn): string => item.status == 'queued' ? '问题已收到,等待AI回复…' : (item.status == 'running' ? '正在整理建议…' : (item.status == 'local' ? (sending.value ? '正在确认发送结果…' : '尚未确认发送结果,请查看下方提示。') : '回复暂未完成。' + (item.canRetry ? '您可以重新尝试。' : '已达到本条消息重试上限。')))
|
||||||
const source = () => {
|
const source = () => {
|
||||||
if (assessmentId.value.length == 0) { uni.reLaunch({ url: '/pages/home/home' }); return }
|
if (assessmentId.value.length == 0) { uni.reLaunch({ url: '/pages/home/home' }); return }
|
||||||
let url = '/packages/finance/report/report?assessmentId=' + assessmentId.value
|
let url = '/packages/finance/report/report?assessmentId=' + assessmentId.value + (revisionId.value.length > 0 ? '&revisionId=' + revisionId.value : '')
|
||||||
if (from.value == 'product' && productIds.value.length == 1) url = '/packages/finance/product/product?assessmentId=' + assessmentId.value + '&productId=' + productIds.value[0] + '&tab=explanation'
|
if (from.value == 'product' && productIds.value.length == 1) url = '/packages/finance/product/product?assessmentId=' + assessmentId.value + (revisionId.value.length > 0 ? '&revisionId=' + revisionId.value : '') + '&productId=' + productIds.value[0] + '&tab=explanation'
|
||||||
if (from.value == 'compare') url = '/packages/finance/compare/compare?assessmentId=' + assessmentId.value + '&productIds=' + productIds.value.join(',')
|
if (from.value == 'compare') url = '/packages/finance/compare/compare?assessmentId=' + assessmentId.value + (revisionId.value.length > 0 ? '&revisionId=' + revisionId.value : '') + '&productIds=' + productIds.value.join(',')
|
||||||
if (from.value == 'next-step') url = '/packages/finance/next-step/next-step?assessmentId=' + assessmentId.value + (productIds.value.length == 1 ? '&productId=' + productIds.value[0] + '§ion=prepare' : '§ion=actions')
|
if (from.value == 'next-step') url = '/packages/finance/next-step/next-step?assessmentId=' + assessmentId.value + (revisionId.value.length > 0 ? '&revisionId=' + revisionId.value : '') + (productIds.value.length == 1 ? '&productId=' + productIds.value[0] + '§ion=prepare' : '§ion=actions')
|
||||||
uni.redirectTo({ url })
|
uni.redirectTo({ url })
|
||||||
}
|
}
|
||||||
const back = () => { if (getCurrentPages().length > 1) uni.navigateBack(); else source() }
|
const back = () => { if (getCurrentPages().length > 1) uni.navigateBack(); else source() }
|
||||||
const login = () => { uni.redirectTo({ url: '/packages/account/login/login?resumeAssistant=1&assessmentId=' + assessmentId.value + '&productIds=' + productIds.value.join(',') + '&chatId=' + chatId.value + '&from=' + from.value }) }
|
const login = () => { uni.redirectTo({ url: '/packages/account/login/login?resumeAssistant=1&assessmentId=' + assessmentId.value + (revisionId.value.length > 0 ? '&revisionId=' + revisionId.value : '') + '&productIds=' + productIds.value.join(',') + '&chatId=' + chatId.value + '&from=' + from.value }) }
|
||||||
const stopTimer = () => { if (timer != 0) { clearInterval(timer); timer = 0 } }
|
const stopTimer = () => { if (timer != 0) { clearInterval(timer); timer = 0 } }
|
||||||
const clearContent = () => { context.value = null; contextOpen.value = false; messages.value = []; draft.value = ''; sendText = ''; sendKey = ''; uncertain.value = false; histories.value = []; historyCursor = ''; historyOpen.value = false; sendError.value = ''; queryError.value = '' }
|
const clearContent = () => { context.value = null; contextOpen.value = false; messages.value = []; draft.value = ''; sendText = ''; sendKey = ''; uncertain.value = false; histories.value = []; historyCursor = ''; historyOpen.value = false; sendError.value = ''; queryError.value = '' }
|
||||||
// 请求代次与登录Token双重校验;离页、退出、换账号后旧响应不得回填个人对话。
|
// 请求代次与登录Token双重校验;离页、退出、换账号后旧响应不得回填个人对话。
|
||||||
@@ -112,13 +110,13 @@
|
|||||||
scope = session.getString('token') ?? ''; const token = scope; loading.value = true
|
scope = session.getString('token') ?? ''; const token = scope; loading.value = true
|
||||||
try {
|
try {
|
||||||
if (chatId.value.length > 0) {
|
if (chatId.value.length > 0) {
|
||||||
const chat = await loadAssistant(chatId.value, assessmentId.value, productIds.value, bindingSpecified)
|
const chat = await loadAssistant(chatId.value, assessmentId.value, productIds.value, bindingSpecified, revisionId.value)
|
||||||
if (!current(ticket, token)) return
|
if (!current(ticket, token)) return
|
||||||
const page = await assistantMessages(chatId.value, '')
|
const page = await assistantMessages(chatId.value, '')
|
||||||
if (!current(ticket, token)) return
|
if (!current(ticket, token)) return
|
||||||
context.value = chat.context; assessmentId.value = chat.context.assessmentId; productIds.value = chat.context.productIds
|
context.value = chat.context; revisionId.value = chat.context.revisionId; assessmentId.value = chat.context.assessmentId; productIds.value = chat.context.productIds
|
||||||
messages.value = page.items; more.value = page.more; cursor.value = page.cursor; scrollEnd()
|
messages.value = page.items; more.value = page.more; cursor.value = page.cursor; scrollEnd()
|
||||||
} else { const data = await previewAssistant(assessmentId.value, productIds.value); if (!current(ticket, token)) return; context.value = data }
|
} else { const data = await previewAssistant(assessmentId.value, productIds.value, revisionId.value); if (!current(ticket, token)) return; context.value = data; revisionId.value = data.revisionId }
|
||||||
pollCount = 0; pollStopped.value = false; beginTimer()
|
pollCount = 0; pollStopped.value = false; beginTimer()
|
||||||
} catch (error) { if (current(ticket, token)) { context.value = null; messages.value = []; pageError.value = error instanceof Error ? error.message : '对话读取失败' } }
|
} catch (error) { if (current(ticket, token)) { context.value = null; messages.value = []; pageError.value = error instanceof Error ? error.message : '对话读取失败' } }
|
||||||
finally { if (ticket == generation) loading.value = false }
|
finally { if (ticket == generation) loading.value = false }
|
||||||
@@ -133,14 +131,14 @@
|
|||||||
const updateUrl = () => {
|
const updateUrl = () => {
|
||||||
// H5仅更新地址中的ID,刷新后重新鉴权;不把消息、Token或报告写入URL/本地缓存。
|
// H5仅更新地址中的ID,刷新后重新鉴权;不把消息、Token或报告写入URL/本地缓存。
|
||||||
// #ifdef WEB
|
// #ifdef WEB
|
||||||
history.replaceState(history.state, '', location.pathname + location.search + '#' + assistantRoute(assessmentId.value, productIds.value, from.value, chatId.value))
|
history.replaceState(history.state, '', location.pathname + location.search + '#' + assistantRoute(assessmentId.value, productIds.value, from.value, chatId.value, revisionId.value))
|
||||||
// #endif
|
// #endif
|
||||||
}
|
}
|
||||||
const transmit = async () => {
|
const transmit = async () => {
|
||||||
if (sending.value || context.value == null) return
|
if (sending.value || context.value == null) return
|
||||||
const ticket = generation; const token = scope; sending.value = true; sendError.value = ''; uncertain.value = true
|
const ticket = generation; const token = scope; sending.value = true; sendError.value = ''; uncertain.value = true
|
||||||
try {
|
try {
|
||||||
if (chatId.value.length == 0) { const chat = await createAssistant(assessmentId.value, productIds.value, createKey); if (!current(ticket, token)) return; chatId.value = chat.id; updateUrl() }
|
if (chatId.value.length == 0) { const chat = await createAssistant(assessmentId.value, productIds.value, createKey, revisionId.value); if (!current(ticket, token)) return; chatId.value = chat.id; updateUrl() }
|
||||||
const item = await submitAssistant(chatId.value, sendKey, sendText, sendAttempt)
|
const item = await submitAssistant(chatId.value, sendKey, sendText, sendAttempt)
|
||||||
if (!current(ticket, token)) return
|
if (!current(ticket, token)) return
|
||||||
merge([item]); uncertain.value = false; draft.value = ''; pollCount = 0; pollStopped.value = false; scrollEnd()
|
merge([item]); uncertain.value = false; draft.value = ''; pollCount = 0; pollStopped.value = false; scrollEnd()
|
||||||
@@ -160,8 +158,10 @@
|
|||||||
}
|
}
|
||||||
const showHistory = () => { historyOpen.value = !historyOpen.value; if (historyOpen.value && histories.value.length == 0) loadHistory() }
|
const showHistory = () => { historyOpen.value = !historyOpen.value; if (historyOpen.value && histories.value.length == 0) loadHistory() }
|
||||||
const openHistory = (id: string) => { uni.redirectTo({ url: assistantRoute('', [], 'archive', id) }) }
|
const openHistory = (id: string) => { uni.redirectTo({ url: assistantRoute('', [], 'archive', id) }) }
|
||||||
const newChat = () => { uni.redirectTo({ url: assistantRoute(assessmentId.value, productIds.value, from.value) }) }
|
const newChat = () => { uni.redirectTo({ url: assistantRoute(assessmentId.value, productIds.value, from.value, '', revisionId.value) }) }
|
||||||
onLoad((options) => {
|
onLoad((options) => {
|
||||||
|
revisionId.value = options?.['revisionId'] ?? '';
|
||||||
|
if (revisionId.value.length > 0 && !validRecordId(revisionId.value)) routeError.value = '版本编号无效,请返回重新选择。'
|
||||||
assessmentId.value = options?.['assessmentId'] ?? ''; chatId.value = options?.['chatId'] ?? ''; from.value = options?.['from'] ?? ''; bindingSpecified = options?.['assessmentId'] != null || options?.['productIds'] != null
|
assessmentId.value = options?.['assessmentId'] ?? ''; chatId.value = options?.['chatId'] ?? ''; from.value = options?.['from'] ?? ''; bindingSpecified = options?.['assessmentId'] != null || options?.['productIds'] != null
|
||||||
try {
|
try {
|
||||||
productIds.value = parseCompareIds(options?.['productIds'] ?? '')
|
productIds.value = parseCompareIds(options?.['productIds'] ?? '')
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
<view v-for="column in columns" :key="column.id" class="overview-value"><text :class="overviewClass(column, row.id)">{{ cell(column, row.id) }}</text></view>
|
<view v-for="column in columns" :key="column.id" class="overview-value"><text :class="overviewClass(column, row.id)">{{ cell(column, row.id) }}</text></view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<text class="overview-note">{{ hasDemo ? '含演示产品,仅供功能验证。' : '' }}展示顺序不代表优劣。</text>
|
<text class="overview-note">展示顺序不代表优劣,具体条件以机构核验为准。</text>
|
||||||
<view class="details-toggle" @click="detailsOpen = !detailsOpen"><text class="summary-title">详细条件与匹配依据</text><text class="detail-toggle-label">{{ detailsOpen ? '收起' : '展开' }}</text></view>
|
<view class="details-toggle" @click="detailsOpen = !detailsOpen"><text class="summary-title">详细条件与匹配依据</text><text class="detail-toggle-label">{{ detailsOpen ? '收起' : '展开' }}</text></view>
|
||||||
<text v-if="detailsOpen" class="scroll-hint">左右滑动查看各产品 · 长内容可在单元格内上下滑动</text>
|
<text v-if="detailsOpen" class="scroll-hint">左右滑动查看各产品 · 长内容可在单元格内上下滑动</text>
|
||||||
<view v-if="detailsOpen" class="table">
|
<view v-if="detailsOpen" class="table">
|
||||||
@@ -40,7 +40,6 @@
|
|||||||
<text class="product-name">{{ column.data!.item.product.name }}</text>
|
<text class="product-name">{{ column.data!.item.product.name }}</text>
|
||||||
<text class="hint">{{ column.data!.item.product.bank }} · v{{ column.data!.item.product.version }}</text>
|
<text class="hint">{{ column.data!.item.product.bank }} · v{{ column.data!.item.product.version }}</text>
|
||||||
<text :class="column.data!.item.state == 'eligible' ? 'state good' : (column.data!.item.state == 'ineligible' ? 'state blocked' : 'state pending')">{{ column.data!.item.stateLabel }}</text>
|
<text :class="column.data!.item.state == 'eligible' ? 'state good' : (column.data!.item.state == 'ineligible' ? 'state blocked' : 'state pending')">{{ column.data!.item.stateLabel }}</text>
|
||||||
<text v-if="column.data!.item.product.demo" class="demo">演示产品 · 非真实申请</text>
|
|
||||||
<text class="detail" @click="detail(column.id)">查看依据与详情 ›</text>
|
<text class="detail" @click="detail(column.id)">查看依据与详情 ›</text>
|
||||||
</template>
|
</template>
|
||||||
<template v-else><text class="product-name">产品 #{{ column.id }}</text><text class="hint">{{ column.loading ? '正在读取历史资料…' : problem(column) }}</text><button v-if="!column.loading" class="retry" @click="loadColumn(column.id)">重试此列</button></template>
|
<template v-else><text class="product-name">产品 #{{ column.id }}</text><text class="hint">{{ column.loading ? '正在读取历史资料…' : problem(column) }}</text><button v-if="!column.loading" class="retry" @click="loadColumn(column.id)">重试此列</button></template>
|
||||||
@@ -54,7 +53,7 @@
|
|||||||
</scroll-view>
|
</scroll-view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
<text class="footer-note">历史参数保留评估时版本;当前可用状态单独展示。缺失信息不会被推断为零,演示产品仅供功能验证。</text>
|
<text class="footer-note">历史参数保留评估时版本;当前可用状态单独展示。缺失信息不会被推断为零。</text>
|
||||||
<button class="secondary" @click="consult">产品差异,咨询AI</button>
|
<button class="secondary" @click="consult">产品差异,咨询AI</button>
|
||||||
<button class="secondary" @click="toMatches">返回本次匹配</button>
|
<button class="secondary" @click="toMatches">返回本次匹配</button>
|
||||||
</template>
|
</template>
|
||||||
@@ -72,13 +71,13 @@
|
|||||||
type CompareColumn = { id: string, loading: boolean, error: string, data: MatchDetail | null }
|
type CompareColumn = { id: string, loading: boolean, error: string, data: MatchDetail | null }
|
||||||
type CompareRow = { id: string, label: string, height: number }
|
type CompareRow = { id: string, label: string, height: number }
|
||||||
const { pageStyle, contentStyle } = usePageLayout()
|
const { pageStyle, contentStyle } = usePageLayout()
|
||||||
const assessmentId = ref(''); const ids = ref<string[]>([]); const columns = ref<CompareColumn[]>([]); const routeError = ref('')
|
const revisionId = ref(''); const assessmentId = ref(''); const ids = ref<string[]>([]); const columns = ref<CompareColumn[]>([]); const routeError = ref('')
|
||||||
const detailsOpen = ref(false)
|
const detailsOpen = ref(false)
|
||||||
// 首屏并排展示短参数;长证据保留原宽表,展开后读取完整内容,不截断判断依据。
|
// 首屏并排展示短参数;长证据保留原宽表,展开后读取完整内容,不截断判断依据。
|
||||||
const overviewRows: CompareRow[] = [{ id: 'state', label: '匹配状态', height: 0 }, { id: 'amount', label: '额度范围', height: 0 }, { id: 'term', label: '期限范围', height: 0 }, { id: 'rate', label: '年化利率', height: 0 }]
|
const overviewRows: CompareRow[] = [{ id: 'state', label: '匹配状态', height: 0 }, { id: 'amount', label: '额度范围', height: 0 }, { id: 'term', label: '期限范围', height: 0 }, { id: 'rate', label: '年化利率', height: 0 }]
|
||||||
let visible = false; let generation = 0
|
let visible = false; let generation = 0
|
||||||
const rows: CompareRow[] = [
|
const rows: CompareRow[] = [
|
||||||
{ id: 'availability', label: '当前状态', height: 160 }, { id: 'coverage', label: '需求覆盖依据', height: 360 },
|
{ id: 'availability', label: '当前状态', height: 160 },
|
||||||
{ id: 'amount', label: '额度范围', height: 120 }, { id: 'term', label: '期限范围', height: 120 }, { id: 'rate', label: '年化利率', height: 120 },
|
{ id: 'amount', label: '额度范围', height: 120 }, { id: 'term', label: '期限范围', height: 120 }, { id: 'rate', label: '年化利率', height: 120 },
|
||||||
{ id: 'objects', label: '适用对象', height: 120 }, { id: 'regions', label: '适用地区', height: 140 },
|
{ id: 'objects', label: '适用对象', height: 120 }, { id: 'regions', label: '适用地区', height: 140 },
|
||||||
{ id: 'positive', label: '已满足条件', height: 360 }, { id: 'blocking', label: '不符条件', height: 300 }, { id: 'cautions', label: '待核实与提示', height: 300 },
|
{ id: 'positive', label: '已满足条件', height: 360 }, { id: 'blocking', label: '不符条件', height: 300 }, { id: 'cautions', label: '待核实与提示', height: 300 },
|
||||||
@@ -96,7 +95,6 @@
|
|||||||
}
|
}
|
||||||
const usable = (column: CompareColumn): boolean => !column.loading && column.data != null && problem(column).length == 0
|
const usable = (column: CompareColumn): boolean => !column.loading && column.data != null && problem(column).length == 0
|
||||||
const complete = computed((): boolean => ids.value.length >= 2 && columns.value.length == ids.value.length && columns.value.every((c: CompareColumn): boolean => usable(c)))
|
const complete = computed((): boolean => ids.value.length >= 2 && columns.value.length == ids.value.length && columns.value.every((c: CompareColumn): boolean => usable(c)))
|
||||||
const hasDemo = computed((): boolean => columns.value.some((c: CompareColumn): boolean => usable(c) && c.data!.item.product.demo))
|
|
||||||
const overviewClass = (column: CompareColumn, key: string): string => {
|
const overviewClass = (column: CompareColumn, key: string): string => {
|
||||||
if (key == 'amount') return 'overview-text overview-amount'
|
if (key == 'amount') return 'overview-text overview-amount'
|
||||||
if (key != 'state' || !usable(column)) return 'overview-text'
|
if (key != 'state' || !usable(column)) return 'overview-text'
|
||||||
@@ -108,10 +106,6 @@
|
|||||||
// 核心表也保留当前不可用提示,不能让历史符合条件被误读为现在仍可办理。
|
// 核心表也保留当前不可用提示,不能让历史符合条件被误读为现在仍可办理。
|
||||||
if (key == 'state') return m.stateLabel + (m.availability != 'available' ? ' · ' + m.availabilityLabel : '')
|
if (key == 'state') return m.stateLabel + (m.availability != 'available' ? ' · ' + m.availabilityLabel : '')
|
||||||
if (key == 'availability') return m.availabilityLabel + ';不改变历史结论'
|
if (key == 'availability') return m.availabilityLabel + ';不改变历史结论'
|
||||||
if (key == 'coverage') {
|
|
||||||
const demand = m.positive.concat(m.blocking).concat(m.cautions).filter((r: MatchReason): boolean => r.fields.some((f: string): boolean => ['requestedAmountYuan','requestedTermMonths','purposeCode'].includes(f)))
|
|
||||||
return compareReasonText(demand, '未提供需求覆盖依据,请查看完整匹配详情。')
|
|
||||||
}
|
|
||||||
if (key == 'positive') return compareReasonText(m.positive, '暂无已确认满足的条件')
|
if (key == 'positive') return compareReasonText(m.positive, '暂无已确认满足的条件')
|
||||||
if (key == 'blocking') return compareReasonText(m.blocking, '未发现已知不符条件;仍以核验为准')
|
if (key == 'blocking') return compareReasonText(m.blocking, '未发现已知不符条件;仍以核验为准')
|
||||||
if (key == 'cautions') return compareReasonText(m.cautions, '暂无待核实提示;不代表已获审批')
|
if (key == 'cautions') return compareReasonText(m.cautions, '暂无待核实提示;不代表已获审批')
|
||||||
@@ -134,8 +128,8 @@
|
|||||||
const token = session.getString('token') ?? ''; const request = generation
|
const token = session.getString('token') ?? ''; const request = generation
|
||||||
column.loading = true; column.error = ''; column.data = null
|
column.loading = true; column.error = ''; column.data = null
|
||||||
try {
|
try {
|
||||||
const result = await fetchMatchDetailRecord(assessmentId.value, id)
|
const result = await fetchMatchDetailRecord(assessmentId.value, id, revisionId.value)
|
||||||
if (visible && request == generation && getSession()?.getString('token') == token) column.data = result
|
if (visible && request == generation && getSession()?.getString('token') == token) { if (revisionId.value.length == 0) revisionId.value = result.revisionId; if (revisionId.value != result.revisionId) throw new Error('评估版本已变化,请重新加载对比。'); column.data = result }
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!visible || request != generation) return
|
if (!visible || request != generation) return
|
||||||
if (getSession()?.getString('token') != token) {
|
if (getSession()?.getString('token') != token) {
|
||||||
@@ -150,21 +144,23 @@
|
|||||||
}
|
}
|
||||||
const toMatches = () => {
|
const toMatches = () => {
|
||||||
if (!validRecordId(assessmentId.value)) { uni.redirectTo({ url: '/packages/finance/archive/archive' }); return }
|
if (!validRecordId(assessmentId.value)) { uni.redirectTo({ url: '/packages/finance/archive/archive' }); return }
|
||||||
uni.redirectTo({ url: '/packages/finance/matches/matches' + compareQuery(assessmentId.value, ids.value) })
|
uni.redirectTo({ url: '/packages/finance/matches/matches' + compareQuery(assessmentId.value, ids.value, revisionId.value) })
|
||||||
}
|
}
|
||||||
const back = () => { if (getCurrentPages().length > 1) uni.navigateBack(); else toMatches() }
|
const back = () => { if (getCurrentPages().length > 1) uni.navigateBack(); else toMatches() }
|
||||||
const consult = () => { uni.navigateTo({ url: assistantRoute(assessmentId.value, ids.value, 'compare') }) }
|
const consult = () => { uni.navigateTo({ url: assistantRoute(assessmentId.value, ids.value, 'compare', '', revisionId.value) }) }
|
||||||
const remove = (id: string) => {
|
const remove = (id: string) => {
|
||||||
const next = ids.value.filter((value: string): boolean => value != id)
|
const next = ids.value.filter((value: string): boolean => value != id)
|
||||||
saveCompareSelection(assessmentId.value, next)
|
saveCompareSelection(assessmentId.value, next, revisionId.value)
|
||||||
// 用新路由替换当前页,使刷新/分享恢复实际选择,而不是已移除的旧ID。
|
// 用新路由替换当前页,使刷新/分享恢复实际选择,而不是已移除的旧ID。
|
||||||
uni.redirectTo({ url: '/packages/finance/compare/compare' + compareQuery(assessmentId.value, next) })
|
uni.redirectTo({ url: '/packages/finance/compare/compare' + compareQuery(assessmentId.value, next, revisionId.value) })
|
||||||
}
|
}
|
||||||
const detail = (id: string) => { uni.navigateTo({ url: '/packages/finance/product/product' + compareQuery(assessmentId.value, ids.value) + '&productId=' + id + '&tab=explanation&from=compare' }) }
|
const detail = (id: string) => { uni.navigateTo({ url: '/packages/finance/product/product' + compareQuery(assessmentId.value, ids.value, revisionId.value) + '&productId=' + id + '&tab=explanation&from=compare' }) }
|
||||||
onLoad((options) => {
|
onLoad((options) => {
|
||||||
|
revisionId.value = options?.['revisionId'] ?? '';
|
||||||
|
if (revisionId.value.length > 0 && !validRecordId(revisionId.value)) routeError.value = '版本编号无效,请返回重新选择。'
|
||||||
assessmentId.value = options?.['assessmentId'] ?? ''
|
assessmentId.value = options?.['assessmentId'] ?? ''
|
||||||
if (!validRecordId(assessmentId.value)) { routeError.value = '评估编号无效,请从融资档案重新选择。'; return }
|
if (!validRecordId(assessmentId.value)) { routeError.value = '评估编号无效,请从融资档案重新选择。'; return }
|
||||||
try { ids.value = parseCompareIds(options?.['productIds'] ?? ''); saveCompareSelection(assessmentId.value, ids.value) }
|
try { ids.value = parseCompareIds(options?.['productIds'] ?? ''); saveCompareSelection(assessmentId.value, ids.value, revisionId.value) }
|
||||||
catch (err) { routeError.value = err instanceof Error ? err.message : '对比参数无效' }
|
catch (err) { routeError.value = err instanceof Error ? err.message : '对比参数无效' }
|
||||||
})
|
})
|
||||||
onShow(() => {
|
onShow(() => {
|
||||||
|
|||||||
@@ -1,16 +1,22 @@
|
|||||||
<template>
|
<template>
|
||||||
<view :class="compact ? 'export-area export-compact' : 'export-area'">
|
<view :class="toolbar ? 'export-area export-toolbar' : (compact ? 'export-area export-compact' : 'export-area')">
|
||||||
<view class="export-row"><text class="export-label">{{ compact ? '评估 #' + assessmentId + ' · 已保存' : '报告已保存至融资档案' }}</text><button class="export-button" :disabled="busy" @click="download">{{ busy ? '正在生成…' : '下载PDF' }}</button></view>
|
<view class="export-row"><text v-if="!toolbar" class="export-label">{{ compact ? '评估 #' + assessmentId + ' · 已保存' : '报告已保存至融资档案' }}</text><button :class="toolbar ? 'export-button toolbar-button' : 'export-button'" :disabled="busy" @click="download">{{ toolbar ? (busy ? '…' : toolbarLabel) : (busy ? '正在生成…' : '下载PDF') }}</button></view>
|
||||||
<text v-if="message.length > 0" :class="failed ? 'export-message export-error' : 'export-message'">{{ message }}</text>
|
<text v-if="message.length > 0 && !toolbar" :class="failed ? 'export-message export-error' : 'export-message'">{{ message }}</text>
|
||||||
<text v-if="message.length == 0 && !compact" class="export-message">导出不含身份信息,请妥善保管下载文件。</text>
|
<text v-if="message.length == 0 && !compact && !toolbar" class="export-message">导出不含身份信息,请妥善保管下载文件。</text>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="uts">
|
<script setup lang="uts">
|
||||||
import { getSession } from '@/services/api.uts'
|
import { getSession } from '@/services/api.uts'
|
||||||
import { ReportDownload, downloadReportPdf } from '@/packages/finance/services/report-export.uts'
|
import { ReportDownload, downloadReportPdf } from '@/packages/finance/services/report-export.uts'
|
||||||
// 报告页使用紧凑入口,下载进度和错误始终展示;其他页面保留原布局。
|
// 顶栏模式由报告页显示完整导出回执,避免窄导航栏中的错误消息被裁切。
|
||||||
const props = defineProps({ assessmentId: { type: String, required: true }, compact: { type: Boolean, default: false } })
|
const props = defineProps({ revisionId: { type: String, default: '' }, assessmentId: { type: String, required: true }, compact: { type: Boolean, default: false }, toolbar: { type: Boolean, default: false } })
|
||||||
const emit = defineEmits(['authrequired', 'sessionchanged'])
|
const emit = defineEmits(['authrequired', 'sessionchanged', 'statuschange'])
|
||||||
|
// #ifdef MP-WEIXIN
|
||||||
|
const toolbarLabel = 'PDF'
|
||||||
|
// #endif
|
||||||
|
// #ifndef MP-WEIXIN
|
||||||
|
const toolbarLabel = '下载PDF'
|
||||||
|
// #endif
|
||||||
const busy = ref(false); const message = ref(''); const failed = ref(false)
|
const busy = ref(false); const message = ref(''); const failed = ref(false)
|
||||||
let operation: ReportDownload | null = null; let generation = 0
|
let operation: ReportDownload | null = null; let generation = 0
|
||||||
const ownerToken = getSession()?.getString('token') ?? ''
|
const ownerToken = getSession()?.getString('token') ?? ''
|
||||||
@@ -20,15 +26,16 @@
|
|||||||
if (getSession() == null) { emit('authrequired'); return }
|
if (getSession() == null) { emit('authrequired'); return }
|
||||||
if (getSession()?.getString('token') != ownerToken) { emit('sessionchanged'); return }
|
if (getSession()?.getString('token') != ownerToken) { emit('sessionchanged'); return }
|
||||||
const request = ++generation; busy.value = true; failed.value = false; message.value = ''
|
const request = ++generation; busy.value = true; failed.value = false; message.value = ''
|
||||||
|
if (props.toolbar) emit('statuschange', '正在生成PDF…')
|
||||||
const token = getSession()?.getString('token') ?? ''
|
const token = getSession()?.getString('token') ?? ''
|
||||||
operation = downloadReportPdf(props.assessmentId)
|
operation = downloadReportPdf(props.assessmentId, props.revisionId)
|
||||||
try { const result = await operation!.done; if (request == generation) message.value = result }
|
try { const result = await operation!.done; if (request == generation) message.value = result }
|
||||||
catch (err) {
|
catch (err) {
|
||||||
if (request != generation) return
|
if (request != generation) return
|
||||||
failed.value = true; message.value = err instanceof Error ? err.message : '下载失败,请检查网络后重试。'
|
failed.value = true; message.value = err instanceof Error ? err.message : '下载失败,请检查网络后重试。'
|
||||||
if (getSession() == null) emit('authrequired')
|
if (getSession() == null) emit('authrequired')
|
||||||
else if (getSession()?.getString('token') != token) emit('sessionchanged')
|
else if (getSession()?.getString('token') != token) emit('sessionchanged')
|
||||||
} finally { if (request == generation) { busy.value = false; operation = null } }
|
} finally { if (request == generation) { busy.value = false; operation = null; if (props.toolbar) emit('statuschange', message.value) } }
|
||||||
}
|
}
|
||||||
// 父页面离开即移除报告,组件销毁时中止下载,避免隐藏页面导出旧账户资料。
|
// 父页面离开即移除报告,组件销毁时中止下载,避免隐藏页面导出旧账户资料。
|
||||||
onUnmounted(() => dispose())
|
onUnmounted(() => dispose())
|
||||||
@@ -41,4 +48,9 @@
|
|||||||
.export-message { color: #8393a6; font-size: 22rpx; line-height: 36rpx; margin-top: 12rpx; }
|
.export-message { color: #8393a6; font-size: 22rpx; line-height: 36rpx; margin-top: 12rpx; }
|
||||||
.export-error { color: #a05d37; }
|
.export-error { color: #a05d37; }
|
||||||
.export-compact { margin-top: 0; margin-bottom: 0; }
|
.export-compact { margin-top: 0; margin-bottom: 0; }
|
||||||
|
.export-toolbar { margin: 0; }
|
||||||
|
.toolbar-button { background-color: #0868f7; color: #fff; margin: 0; padding: 0 16rpx; height: 50rpx; line-height: 50rpx; font-size: 21rpx; border-radius: 12rpx; }
|
||||||
|
/* #ifdef MP-WEIXIN */
|
||||||
|
.toolbar-button { width: 32px; height: 32px; line-height: 32px; padding: 0; font-size: 11px; }
|
||||||
|
/* #endif */
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<view class="confirm-tip"><view class="tip-icon"><text class="tip-mark">✓</text></view><view class="tip-copy"><text class="tip-title">{{ assessmentType == 'enterprise' ? '企业融资信息' : '个人融资信息' }}</text><text class="tip-desc">请确认后再开始评估,可随时返回修改。</text></view></view>
|
<view class="confirm-tip"><view class="tip-icon"><text class="tip-mark">✓</text></view><view class="tip-copy"><text class="tip-title">{{ assessmentType == 'enterprise' ? '企业融资信息' : '个人融资信息' }}</text><text class="tip-desc">请确认后再开始评估,可随时返回修改。</text></view></view>
|
||||||
<text v-if="!hasInput" class="empty-hint">还没有评估信息,请先完成信息填写。</text>
|
<text v-if="!hasInput" class="empty-hint">还没有评估信息,请先完成信息填写。</text>
|
||||||
<view class="info-section" v-for="section in sections" :key="section.title"><view class="section-bar"><text class="section-name">{{ section.title }}</text><text class="edit" @click="edit(section.step)">编辑</text></view><template v-for="(row, index) in section.rows" :key="row.label"><view v-if="index < 3 || section.expanded" class="info-row"><text class="info-label">{{ row.label }}</text><text class="info-value">{{ row.value }}</text></view></template><text v-if="section.rows.length > 3" class="expand-link" @click="section.expanded = !section.expanded">{{ section.expanded ? '收起详情' : '查看其余信息' }}</text></view>
|
<view class="info-section" v-for="section in sections" :key="section.title"><view class="section-bar"><text class="section-name">{{ section.title }}</text><text class="edit" @click="edit(section.step)">编辑</text></view><template v-for="(row, index) in section.rows" :key="row.label"><view v-if="index < 3 || section.expanded" class="info-row"><text class="info-label">{{ row.label }}</text><text class="info-value">{{ row.value }}</text></view></template><text v-if="section.rows.length > 3" class="expand-link" @click="section.expanded = !section.expanded">{{ section.expanded ? '收起详情' : '查看其余信息' }}</text></view>
|
||||||
<text class="assessment-note">提交后保存评估资料并进行自填资料分析。融资需求不代表授信承诺。资料上传及外部查询暂未开放;如已选资料或授权,请先移除。不会向金融机构提交贷款申请。</text>
|
<text class="assessment-note">提交后保存评估资料并进行自填资料分析。评估结果不代表授信承诺。资料上传及外部查询暂未开放;如已选资料或授权,请先移除。不会向金融机构提交贷款申请。</text>
|
||||||
<view class="scroll-spacer" :style="bottomStyle(24)"></view>
|
<view class="scroll-spacer" :style="bottomStyle(24)"></view>
|
||||||
</view>
|
</view>
|
||||||
</scroll-view>
|
</scroll-view>
|
||||||
@@ -18,7 +18,6 @@
|
|||||||
<script setup lang="uts">
|
<script setup lang="uts">
|
||||||
|
|
||||||
import { wanAmountValid } from '@/services/money.uts'
|
import { wanAmountValid } from '@/services/money.uts'
|
||||||
import { demandValidationMessage } from '@/services/assessment-demand.uts'
|
|
||||||
import { usePageLayout } from '@/services/layout.uts'
|
import { usePageLayout } from '@/services/layout.uts'
|
||||||
import AppHeader from '@/components/AppHeader.uvue'
|
import AppHeader from '@/components/AppHeader.uvue'
|
||||||
import { submitAssessment } from '@/packages/finance/services/api.uts'
|
import { submitAssessment } from '@/packages/finance/services/api.uts'
|
||||||
@@ -59,12 +58,6 @@
|
|||||||
{ label: '职业类型', value: displayValue(saved, 'occupation') }, { label: '月收入', value: draftText(saved, 'monthlyIncome').length > 0 ? draftText(saved, 'monthlyIncome') + ' 万元' : '未填写' }, { label: '工作单位', value: displayValue(saved, 'company') }, { label: '社保缴纳', value: displayValue(saved, 'socialSecurity') }, { label: '公积金缴纳', value: displayValue(saved, 'housingFund') }
|
{ label: '职业类型', value: displayValue(saved, 'occupation') }, { label: '月收入', value: draftText(saved, 'monthlyIncome').length > 0 ? draftText(saved, 'monthlyIncome') + ' 万元' : '未填写' }, { label: '工作单位', value: displayValue(saved, 'company') }, { label: '社保缴纳', value: displayValue(saved, 'socialSecurity') }, { label: '公积金缴纳', value: displayValue(saved, 'housingFund') }
|
||||||
]
|
]
|
||||||
const files = documentsFromInput(saved)
|
const files = documentsFromInput(saved)
|
||||||
const demandRows: InfoRow[] = [
|
|
||||||
{ label: '融资金额', value: valueWithUnit(saved, 'requestedAmountWan', '万元') },
|
|
||||||
{ label: '融资期限', value: valueWithUnit(saved, 'requestedTermMonths', '个月') },
|
|
||||||
{ label: '资金用途', value: displayValue(saved, 'purposeLabel') }
|
|
||||||
]
|
|
||||||
if (draftText(saved, 'purposeCode') == 'other') appendRow(demandRows, '用途说明', displayValue(saved, 'purposeDetail'))
|
|
||||||
const fileNames = files.map((file) => file.name).join('、')
|
const fileNames = files.map((file) => file.name).join('、')
|
||||||
const grants = saved['authorizations'] as string[] | null
|
const grants = saved['authorizations'] as string[] | null
|
||||||
const grantNames: string[] = []
|
const grantNames: string[] = []
|
||||||
@@ -72,7 +65,6 @@
|
|||||||
sections.value = [
|
sections.value = [
|
||||||
{ title: '基本信息', step: 0, rows: basicRows, expanded: false },
|
{ title: '基本信息', step: 0, rows: basicRows, expanded: false },
|
||||||
{ title: enterprise ? '经营信息' : '职业与收入', step: 1, rows: incomeRows, expanded: false },
|
{ title: enterprise ? '经营信息' : '职业与收入', step: 1, rows: incomeRows, expanded: false },
|
||||||
{ title: '资金需求', step: 1, rows: demandRows, expanded: true },
|
|
||||||
{ title: '资产与负债', step: 2, rows: [
|
{ title: '资产与负债', step: 2, rows: [
|
||||||
{ label: '信用贷款笔数', value: valueWithUnit(saved, 'creditLoanCount', '笔') },
|
{ label: '信用贷款笔数', value: valueWithUnit(saved, 'creditLoanCount', '笔') },
|
||||||
{ label: '欠款总余额', value: valueWithUnit(saved, 'totalDebtBalance', '万元') },
|
{ label: '欠款总余额', value: valueWithUnit(saved, 'totalDebtBalance', '万元') },
|
||||||
@@ -108,8 +100,6 @@
|
|||||||
if (assessmentType.value == 'personal' && !/(^[0-9]{15}$)|(^[0-9]{17}[0-9Xx]$)/.test(draftText(saved, 'idCard'))) { uni.showToast({ title: '请补全身份证信息', icon: 'none' }); edit(0); return }
|
if (assessmentType.value == 'personal' && !/(^[0-9]{15}$)|(^[0-9]{17}[0-9Xx]$)/.test(draftText(saved, 'idCard'))) { uni.showToast({ title: '请补全身份证信息', icon: 'none' }); edit(0); return }
|
||||||
if (assessmentType.value == 'enterprise' && (draftText(saved, 'enterpriseName').length < 2 || !/^[0-9A-Z]{18}$/.test(draftText(saved, 'businessCode').toUpperCase()))) { uni.showToast({ title: '请补全企业信息', icon: 'none' }); edit(0); return }
|
if (assessmentType.value == 'enterprise' && (draftText(saved, 'enterpriseName').length < 2 || !/^[0-9A-Z]{18}$/.test(draftText(saved, 'businessCode').toUpperCase()))) { uni.showToast({ title: '请补全企业信息', icon: 'none' }); edit(0); return }
|
||||||
if (!wanAmountValid(amount) || parseFloat(amount) <= 0) { uni.showToast({ title: '请先完善收入信息', icon: 'none' }); edit(1); return }
|
if (!wanAmountValid(amount) || parseFloat(amount) <= 0) { uni.showToast({ title: '请先完善收入信息', icon: 'none' }); edit(1); return }
|
||||||
const demandError = demandValidationMessage(draftText(saved, 'requestedAmountWan'), draftText(saved, 'requestedTermMonths'), draftText(saved, 'purposeCode'), draftText(saved, 'purposeDetail'))
|
|
||||||
if (demandError.length > 0) { uni.showToast({ title: demandError, icon: 'none' }); edit(1); return }
|
|
||||||
const grants = saved['authorizations'] as string[] | null
|
const grants = saved['authorizations'] as string[] | null
|
||||||
if ((documentsFromInput(saved).length > 0 && saved['documentConsent'] != true) || (grants != null && grants.length > 0 && saved['dataConsent'] != true)) { uni.showToast({ title: '请确认资料与授权选择', icon: 'none' }); edit(3); return }
|
if ((documentsFromInput(saved).length > 0 && saved['documentConsent'] != true) || (grants != null && grants.length > 0 && saved['dataConsent'] != true)) { uni.showToast({ title: '请确认资料与授权选择', icon: 'none' }); edit(3); return }
|
||||||
const assetError = assetValidationMessage(formFromInput(saved))
|
const assetError = assetValidationMessage(formFromInput(saved))
|
||||||
@@ -118,7 +108,7 @@
|
|||||||
try {
|
try {
|
||||||
const id = await submitAssessment(payload.value)
|
const id = await submitAssessment(payload.value)
|
||||||
clearAssessmentFlow()
|
clearAssessmentFlow()
|
||||||
// 已提交记录只读;清空页面栈,防止返回旧表单再次提交。
|
// 清空本次填写流程和页面栈;修改须重新读取最新版本,防止返回旧表单重复提交。
|
||||||
uni.reLaunch({ url: '/packages/finance/analyzing/analyzing?assessmentId=' + encodeURIComponent(id) })
|
uni.reLaunch({ url: '/packages/finance/analyzing/analyzing?assessmentId=' + encodeURIComponent(id) })
|
||||||
}
|
}
|
||||||
catch (error) { uni.showToast({ title: error instanceof Error ? error.message : '提交失败,请重试', icon: 'none' }) }
|
catch (error) { uni.showToast({ title: error instanceof Error ? error.message : '提交失败,请重试', icon: 'none' }) }
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
<view v-if="!loading && error.length == 0 && matches.length == 0" class="state-panel"><text class="state-title">{{ status == 'unavailable' ? '本次未执行匹配' : (group == 'eligible' ? '暂无符合已知条件的产品' : (group == 'ineligible' ? '暂无条件不符的产品' : '暂无待核实的产品')) }}</text><text class="muted">{{ status == 'unavailable' ? '历史结果不会自动补算。可从融资档案发起新的评估。' : '可切换分组查看其他结果,或返回报告了解资料准备情况。' }}</text><text class="link" @click="report">查看本次报告</text></view>
|
<view v-if="!loading && error.length == 0 && matches.length == 0" class="state-panel"><text class="state-title">{{ status == 'unavailable' ? '本次未执行匹配' : (group == 'eligible' ? '暂无符合已知条件的产品' : (group == 'ineligible' ? '暂无条件不符的产品' : '暂无待核实的产品')) }}</text><text class="muted">{{ status == 'unavailable' ? '历史结果不会自动补算。可从融资档案发起新的评估。' : '可切换分组查看其他结果,或返回报告了解资料准备情况。' }}</text><text class="link" @click="report">查看本次报告</text></view>
|
||||||
<view v-for="item in matches" :key="item.id" class="card" @click="openProduct(item.id)">
|
<view v-for="item in matches" :key="item.id" class="card" @click="openProduct(item.id)">
|
||||||
<view class="card-head"><view class="mark"><text class="mark-text">融</text></view><view class="identity"><text class="name">{{ item.product.name }}</text><text class="bank">{{ item.product.bank }} · {{ item.product.objects }}</text></view></view>
|
<view class="card-head"><view class="mark"><text class="mark-text">融</text></view><view class="identity"><text class="name">{{ item.product.name }}</text><text class="bank">{{ item.product.bank }} · {{ item.product.objects }}</text></view></view>
|
||||||
<view class="badge-row"><text :class="item.state == 'eligible' ? 'badge good' : (item.state == 'ineligible' ? 'badge blocked' : 'badge pending')">{{ item.stateLabel }}</text><text v-if="item.product.demo" class="demo">演示产品</text></view>
|
<view class="badge-row"><text :class="item.state == 'eligible' ? 'badge good' : (item.state == 'ineligible' ? 'badge blocked' : 'badge pending')">{{ item.stateLabel }}</text></view>
|
||||||
<view class="amount-row"><text class="amount">{{ item.product.amount }}</text><text class="caption">产品额度范围</text></view>
|
<view class="amount-row"><text class="amount">{{ item.product.amount }}</text><text class="caption">产品额度范围</text></view>
|
||||||
<view class="metrics"><view class="metric"><text class="value">{{ item.product.rate }}</text><text class="caption">年化利率范围</text></view><view class="metric"><text class="value">{{ item.product.term }}</text><text class="caption">期限范围</text></view></view>
|
<view class="metrics"><view class="metric"><text class="value">{{ item.product.rate }}</text><text class="caption">年化利率范围</text></view><view class="metric"><text class="value">{{ item.product.term }}</text><text class="caption">期限范围</text></view></view>
|
||||||
<view class="reasons"><text class="reason" v-for="reason in preview(item)" :key="reason.id">{{ reason.text }}</text></view>
|
<view class="reasons"><text class="reason" v-for="reason in preview(item)" :key="reason.id">{{ reason.text }}</text></view>
|
||||||
@@ -43,7 +43,7 @@
|
|||||||
const { pageStyle, contentStyle } = usePageLayout()
|
const { pageStyle, contentStyle } = usePageLayout()
|
||||||
type MatchGroup = { id: string, label: string }
|
type MatchGroup = { id: string, label: string }
|
||||||
const groups: MatchGroup[] = [{ id: 'eligible', label: '符合条件' }, { id: 'ineligible', label: '条件不符' }, { id: 'unknown', label: '待核实' }]
|
const groups: MatchGroup[] = [{ id: 'eligible', label: '符合条件' }, { id: 'ineligible', label: '条件不符' }, { id: 'unknown', label: '待核实' }]
|
||||||
const assessmentId = ref(''); const group = ref('eligible'); const matches = ref<MatchView[]>([])
|
const revisionId = ref(''); const assessmentId = ref(''); const group = ref('eligible'); const matches = ref<MatchView[]>([])
|
||||||
const selectedIds = ref<string[]>([])
|
const selectedIds = ref<string[]>([])
|
||||||
const pageActive = ref(false)
|
const pageActive = ref(false)
|
||||||
const loading = ref(false); const error = ref(''); const message = ref(''); const status = ref('')
|
const loading = ref(false); const error = ref(''); const message = ref(''); const status = ref('')
|
||||||
@@ -62,8 +62,9 @@
|
|||||||
if (session == null) { uni.redirectTo({ url: '/packages/account/login/login' }); return }
|
if (session == null) { uni.redirectTo({ url: '/packages/account/login/login' }); return }
|
||||||
const token = session.getString('token') ?? ''; owner = token; loading.value = true
|
const token = session.getString('token') ?? ''; owner = token; loading.value = true
|
||||||
try {
|
try {
|
||||||
const result = await fetchMatchPage(assessmentId.value, group.value, append ? cursor : '')
|
const result = await fetchMatchPage(assessmentId.value, group.value, append ? cursor : '', revisionId.value)
|
||||||
if (!visible || request != generation || getSession()?.getString('token') != token) return
|
if (!visible || request != generation || getSession()?.getString('token') != token) return
|
||||||
|
revisionId.value = result.revisionId
|
||||||
matches.value = append ? matches.value.concat(result.items) : result.items
|
matches.value = append ? matches.value.concat(result.items) : result.items
|
||||||
message.value = result.message; status.value = result.status; eligible.value = result.eligible; ineligible.value = result.ineligible; unknown.value = result.unknown
|
message.value = result.message; status.value = result.status; eligible.value = result.eligible; ineligible.value = result.ineligible; unknown.value = result.unknown
|
||||||
cursor = result.cursor; hasMore.value = result.hasMore
|
cursor = result.cursor; hasMore.value = result.hasMore
|
||||||
@@ -75,7 +76,7 @@
|
|||||||
const loadMore = () => { if (!loading.value && hasMore.value) load(true) }
|
const loadMore = () => { if (!loading.value && hasMore.value) load(true) }
|
||||||
const history = () => { uni.navigateTo({ url: '/packages/finance/archive/archive' }) }
|
const history = () => { uni.navigateTo({ url: '/packages/finance/archive/archive' }) }
|
||||||
const start = () => { uni.navigateTo({ url: '/packages/finance/entry/entry' }) }
|
const start = () => { uni.navigateTo({ url: '/packages/finance/entry/entry' }) }
|
||||||
const report = () => { if (validRecordId(assessmentId.value)) uni.redirectTo({ url: '/packages/finance/report/report?assessmentId=' + assessmentId.value }); else history() }
|
const report = () => { if (validRecordId(assessmentId.value)) uni.redirectTo({ url: '/packages/finance/report/report?assessmentId=' + assessmentId.value + (revisionId.value.length > 0 ? '&revisionId=' + revisionId.value : '') }); else history() }
|
||||||
const back = () => { if (getCurrentPages().length > 1) uni.navigateBack(); else if (assessmentId.value.length > 0) report(); else history() }
|
const back = () => { if (getCurrentPages().length > 1) uni.navigateBack(); else if (assessmentId.value.length > 0) report(); else history() }
|
||||||
const toggleCompare = (id: string) => {
|
const toggleCompare = (id: string) => {
|
||||||
if (selectedIds.value.includes(id)) selectedIds.value = selectedIds.value.filter((value: string): boolean => value != id)
|
if (selectedIds.value.includes(id)) selectedIds.value = selectedIds.value.filter((value: string): boolean => value != id)
|
||||||
@@ -83,22 +84,24 @@
|
|||||||
if (selectedIds.value.length >= 3) { uni.showToast({ title: '最多选择3个产品,请先移除一项', icon: 'none' }); return }
|
if (selectedIds.value.length >= 3) { uni.showToast({ title: '最多选择3个产品,请先移除一项', icon: 'none' }); return }
|
||||||
selectedIds.value.push(id)
|
selectedIds.value.push(id)
|
||||||
}
|
}
|
||||||
saveCompareSelection(assessmentId.value, selectedIds.value)
|
saveCompareSelection(assessmentId.value, selectedIds.value, revisionId.value)
|
||||||
}
|
}
|
||||||
const openCompare = () => { if (selectedIds.value.length >= 2) uni.navigateTo({ url: '/packages/finance/compare/compare' + compareQuery(assessmentId.value, selectedIds.value) }) }
|
const openCompare = () => { if (selectedIds.value.length >= 2) uni.navigateTo({ url: '/packages/finance/compare/compare' + compareQuery(assessmentId.value, selectedIds.value, revisionId.value) }) }
|
||||||
const openProduct = (id: string) => { uni.navigateTo({ url: '/packages/finance/product/product' + compareQuery(assessmentId.value, selectedIds.value) + '&productId=' + id + '&tab=explanation&from=matches' }) }
|
const openProduct = (id: string) => { uni.navigateTo({ url: '/packages/finance/product/product' + compareQuery(assessmentId.value, selectedIds.value, revisionId.value) + '&productId=' + id + '&tab=explanation&from=matches' }) }
|
||||||
onLoad((options) => {
|
onLoad((options) => {
|
||||||
|
revisionId.value = options?.['revisionId'] ?? '';
|
||||||
|
if (revisionId.value.length > 0 && !validRecordId(revisionId.value)) routeError = '版本编号无效,请返回重新选择。'
|
||||||
assessmentId.value = options?.['assessmentId'] ?? ''
|
assessmentId.value = options?.['assessmentId'] ?? ''
|
||||||
const selected = options?.['group'] ?? 'eligible'
|
const selected = options?.['group'] ?? 'eligible'
|
||||||
if (['eligible','ineligible','unknown'].includes(selected)) group.value = selected
|
if (['eligible','ineligible','unknown'].includes(selected)) group.value = selected
|
||||||
else routeError = '匹配分组参数无效,请从报告重新进入。'
|
else routeError = '匹配分组参数无效,请从报告重新进入。'
|
||||||
if (assessmentId.value.length > 0 && !validRecordId(assessmentId.value)) routeError = '评估编号无效,请从融资档案重新选择。'
|
if (assessmentId.value.length > 0 && !validRecordId(assessmentId.value)) routeError = '评估编号无效,请从融资档案重新选择。'
|
||||||
if (options?.['productIds'] != null) {
|
if (options?.['productIds'] != null) {
|
||||||
try { saveCompareSelection(assessmentId.value, parseCompareIds(options!['productIds']!)) }
|
try { saveCompareSelection(assessmentId.value, parseCompareIds(options!['productIds']!), revisionId.value) }
|
||||||
catch (err) { routeError = err instanceof Error ? err.message : '对比参数无效' }
|
catch (err) { routeError = err instanceof Error ? err.message : '对比参数无效' }
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
onShow(() => { visible = true; pageActive.value = true; selectedIds.value = compareSelection(assessmentId.value); if (getSession()?.getString('token') != owner) clear(); load(false) })
|
onShow(() => { visible = true; pageActive.value = true; selectedIds.value = compareSelection(assessmentId.value, revisionId.value); if (getSession()?.getString('token') != owner) clear(); load(false) })
|
||||||
// 先关闭内容渲染再清空资料,避免卸载途中因空数组重新挂载状态节点。
|
// 先关闭内容渲染再清空资料,避免卸载途中因空数组重新挂载状态节点。
|
||||||
onHide(() => { visible = false; pageActive.value = false; generation++; clear(); loading.value = false; error.value = '' })
|
onHide(() => { visible = false; pageActive.value = false; generation++; clear(); loading.value = false; error.value = '' })
|
||||||
onUnload(() => { visible = false; pageActive.value = false; generation++; clear() })
|
onUnload(() => { visible = false; pageActive.value = false; generation++; clear() })
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
<view class="actions">
|
<view class="actions">
|
||||||
<view class="action-row" @click="prepare"><image class="action-icon" src="/packages/finance/static/action-prepare.svg" /><view class="action-copy"><text class="action-title">{{ productId.length > 0 && !canPrepare ? '核对产品条件' : '准备申请' }}</text><text class="action-note">{{ productId.length == 0 ? '选择本次匹配产品,查看材料准备说明' : '核对产品条件,了解所需材料' }}</text></view><text class="chevron">›</text></view>
|
<view class="action-row" @click="prepare"><image class="action-icon" src="/packages/finance/static/action-prepare.svg" /><view class="action-copy"><text class="action-title">{{ productId.length > 0 && !canPrepare ? '核对产品条件' : '准备申请' }}</text><text class="action-note">{{ productId.length == 0 ? '选择本次匹配产品,查看材料准备说明' : '核对产品条件,了解所需材料' }}</text></view><text class="chevron">›</text></view>
|
||||||
<view class="action-row" @click="openReport"><image class="action-icon" src="/packages/finance/static/action-report.svg" /><view class="action-copy"><text class="action-title">查看已保存报告</text><text class="action-note">报告已保存至融资档案,可随时查看</text></view><text class="chevron">›</text></view>
|
<view class="action-row" @click="openReport"><image class="action-icon" src="/packages/finance/static/action-report.svg" /><view class="action-copy"><text class="action-title">查看已保存报告</text><text class="action-note">报告已保存至融资档案,可随时查看</text></view><text class="chevron">›</text></view>
|
||||||
<view class="action-row" @click="optimize"><image class="action-icon" src="/packages/finance/static/action-improve.svg" /><view class="action-copy"><text class="action-title">优化资料</text><text class="action-note">核对待补充与待核实项,再发起新评估</text></view><text class="chevron">›</text></view>
|
<view class="action-row" @click="optimize"><image class="action-icon" src="/packages/finance/static/action-improve.svg" /><view class="action-copy"><text class="action-title">优化资料</text><text class="action-note">核对待补充与待核实项,修改后重新评估</text></view><text class="chevron">›</text></view>
|
||||||
<view class="action-row last" @click="consult"><image class="action-icon" src="/packages/finance/static/action-chat.svg" /><view class="action-copy"><text class="action-title">咨询AI</text><text class="action-note">结合本次报告,为您解释准备方向</text></view><text class="chevron">›</text></view>
|
<view class="action-row last" @click="consult"><image class="action-icon" src="/packages/finance/static/action-chat.svg" /><view class="action-copy"><text class="action-title">咨询AI</text><text class="action-note">结合本次报告,为您解释准备方向</text></view><text class="chevron">›</text></view>
|
||||||
</view>
|
</view>
|
||||||
<view v-if="section == 'prepare'" id="prepare-section" class="panel">
|
<view v-if="section == 'prepare'" id="prepare-section" class="panel">
|
||||||
@@ -20,7 +20,6 @@
|
|||||||
<template v-else-if="matchError.length > 0"><text class="body">{{ matchError }}</text><button class="secondary" @click="load">重新核对产品</button></template>
|
<template v-else-if="matchError.length > 0"><text class="body">{{ matchError }}</text><button class="secondary" @click="load">重新核对产品</button></template>
|
||||||
<template v-else-if="match != null">
|
<template v-else-if="match != null">
|
||||||
<text class="product-title">{{ match!.product.name }}</text><text class="note">{{ match!.product.bank }} · 评估时版本 v{{ match!.product.version }}</text>
|
<text class="product-title">{{ match!.product.name }}</text><text class="note">{{ match!.product.bank }} · 评估时版本 v{{ match!.product.version }}</text>
|
||||||
<text v-if="match!.product.demo" class="demo">演示产品,仅用于功能验证,不代表真实在售产品。</text>
|
|
||||||
<text class="status">{{ match!.stateLabel }} · {{ match!.availabilityLabel }}</text>
|
<text class="status">{{ match!.stateLabel }} · {{ match!.availabilityLabel }}</text>
|
||||||
<template v-if="canPrepare">
|
<template v-if="canPrepare">
|
||||||
<text class="label">所需材料与办理说明</text><text class="body">{{ match!.product.materials }}</text>
|
<text class="label">所需材料与办理说明</text><text class="body">{{ match!.product.materials }}</text>
|
||||||
@@ -38,10 +37,10 @@
|
|||||||
<view v-for="item in missingItems" :key="item.label" class="field-row"><text class="field-label">{{ item.label }}</text><text class="field-state">{{ item.value }}</text></view>
|
<view v-for="item in missingItems" :key="item.label" class="field-row"><text class="field-label">{{ item.label }}</text><text class="field-state">{{ item.value }}</text></view>
|
||||||
<text v-if="missingItems.length == 0" class="body">{{ report!.version == 1 ? '旧版报告未记录逐项完整度,请参考以下报告提示。' : '本次报告没有列出待补充或格式需核实字段,自填资料仍需核验。' }}</text>
|
<text v-if="missingItems.length == 0" class="body">{{ report!.version == 1 ? '旧版报告未记录逐项完整度,请参考以下报告提示。' : '本次报告没有列出待补充或格式需核实字段,自填资料仍需核验。' }}</text>
|
||||||
<text v-for="item in report!.dataGaps" :key="item" class="body">{{ item }}</text><text v-for="item in report!.suggestions" :key="item" class="body">{{ item }}</text>
|
<text v-for="item in report!.dataGaps" :key="item" class="body">{{ item }}</text><text v-for="item in report!.suggestions" :key="item" class="body">{{ item }}</text>
|
||||||
<text class="note">将进入同类型空白表单,不带入旧资料。正式提交后生成新记录,原报告保持不变。</text><button class="primary" @click="restart">重新进行{{ assessmentType == 'enterprise' ? '企业' : '个人' }}评估</button>
|
<text class="note">将回填本评估最新提交的资料。修改后保存为新的提交版本,历史资料和报告仍可查看。</text><button class="primary" @click="restart">修改资料并重新评估</button>
|
||||||
</view>
|
</view>
|
||||||
<view class="notice"><text class="notice-title">重要提示</text><text class="notice-text">本页仅提供材料准备与办理引导,不向银行或其他机构提交申请。匹配结果基于自填资料,不代表授信或获批承诺;实际产品条件、材料及办理方式以机构核实为准。</text></view>
|
<view class="notice"><text class="notice-title">重要提示</text><text class="notice-text">本页仅提供材料准备与办理引导,不向银行或其他机构提交申请。匹配结果基于自填资料,不代表授信或获批承诺;实际产品条件、材料及办理方式以机构核实为准。</text></view>
|
||||||
<ReportExport :assessmentId="assessmentId" @authrequired="login" @sessionchanged="load" />
|
<ReportExport :assessmentId="assessmentId" :revisionId="revisionId" @authrequired="login" @sessionchanged="load" />
|
||||||
</template>
|
</template>
|
||||||
<button class="home-button" @click="home">返回首页</button>
|
<button class="home-button" @click="home">返回首页</button>
|
||||||
</view>
|
</view>
|
||||||
@@ -59,7 +58,7 @@
|
|||||||
import { AnalysisReport, AssessmentField, fetchAnalysisReport, fetchAssessmentStatus } from '@/packages/finance/services/records.uts'
|
import { AnalysisReport, AssessmentField, fetchAnalysisReport, fetchAssessmentStatus } from '@/packages/finance/services/records.uts'
|
||||||
import { MatchView, fetchMatchDetailRecord, validRecordId } from '@/packages/finance/services/matches.uts'
|
import { MatchView, fetchMatchDetailRecord, validRecordId } from '@/packages/finance/services/matches.uts'
|
||||||
const { pageStyle, contentStyle } = usePageLayout()
|
const { pageStyle, contentStyle } = usePageLayout()
|
||||||
const assessmentId = ref(''); const productId = ref(''); const section = ref('actions'); const assessmentType = ref('')
|
const revisionId = ref(''); const assessmentId = ref(''); const productId = ref(''); const section = ref('actions'); const assessmentType = ref('')
|
||||||
const report = ref<AnalysisReport | null>(null); const match = ref<MatchView | null>(null)
|
const report = ref<AnalysisReport | null>(null); const match = ref<MatchView | null>(null)
|
||||||
const loading = ref(false); const error = ref(''); const matchError = ref(''); const routeError = ref(''); const improveOpen = ref(false)
|
const loading = ref(false); const error = ref(''); const matchError = ref(''); const routeError = ref(''); const improveOpen = ref(false)
|
||||||
const scrollTarget = ref('')
|
const scrollTarget = ref('')
|
||||||
@@ -69,7 +68,7 @@
|
|||||||
const reset = () => { report.value = null; match.value = null; assessmentType.value = ''; matchError.value = ''; loadedToken = ''; improveOpen.value = false; scrollTarget.value = '' }
|
const reset = () => { report.value = null; match.value = null; assessmentType.value = ''; matchError.value = ''; loadedToken = ''; improveOpen.value = false; scrollTarget.value = '' }
|
||||||
const home = () => { uni.reLaunch({ url: '/pages/home/home' }) }
|
const home = () => { uni.reLaunch({ url: '/pages/home/home' }) }
|
||||||
const history = () => { uni.navigateTo({ url: '/packages/finance/archive/archive' }) }
|
const history = () => { uni.navigateTo({ url: '/packages/finance/archive/archive' }) }
|
||||||
const login = () => { uni.redirectTo({ url: '/packages/account/login/login?resumeNextStep=1&assessmentId=' + assessmentId.value + '&productId=' + productId.value + '§ion=' + section.value }) }
|
const login = () => { uni.redirectTo({ url: '/packages/account/login/login?resumeNextStep=1&assessmentId=' + assessmentId.value + (revisionId.value.length > 0 ? '&revisionId=' + revisionId.value : '') + '&productId=' + productId.value + '§ion=' + section.value }) }
|
||||||
// 离页或账户变更后,丢弃旧请求;重新进入必须重查归属和当前可用性。
|
// 离页或账户变更后,丢弃旧请求;重新进入必须重查归属和当前可用性。
|
||||||
const stale = (request: number, token: string): boolean => {
|
const stale = (request: number, token: string): boolean => {
|
||||||
if (!visible || request != generation) return true
|
if (!visible || request != generation) return true
|
||||||
@@ -87,17 +86,17 @@
|
|||||||
try {
|
try {
|
||||||
const status = await fetchAssessmentStatus(assessmentId.value)
|
const status = await fetchAssessmentStatus(assessmentId.value)
|
||||||
if (stale(request, token)) return
|
if (stale(request, token)) return
|
||||||
if (status.id != assessmentId.value || status.status != 'succeeded' || !['personal', 'enterprise'].includes(status.type)) throw new Error('请从本人已成功生成的报告进入下一步行动。')
|
if (status.id != assessmentId.value || !status.hasReport || !['personal', 'enterprise'].includes(status.type)) throw new Error('请从本人已成功生成的报告进入下一步行动。')
|
||||||
const result = await fetchAnalysisReport(assessmentId.value)
|
const result = await fetchAnalysisReport(assessmentId.value, revisionId.value)
|
||||||
if (stale(request, token)) return
|
if (stale(request, token)) return
|
||||||
report.value = result; assessmentType.value = status.type; loadedToken = token
|
report.value = result; revisionId.value = result.revisionId; assessmentType.value = status.type; loadedToken = token
|
||||||
if (productId.value.length > 0) {
|
if (productId.value.length > 0) {
|
||||||
try {
|
try {
|
||||||
if (result.matching.status != 'completed') throw new Error('本报告没有可读取的产品匹配结果。')
|
if (result.matching.status != 'completed') throw new Error('本报告没有可读取的产品匹配结果。')
|
||||||
const detail = await fetchMatchDetailRecord(assessmentId.value, productId.value)
|
const detail = await fetchMatchDetailRecord(assessmentId.value, productId.value, revisionId.value)
|
||||||
if (stale(request, token)) return
|
if (stale(request, token)) return
|
||||||
// 材料必须来自同次评估冻结产品,不允许用公共或其他报告产品补齐。
|
// 材料必须来自同次评估冻结产品,不允许用公共或其他报告产品补齐。
|
||||||
if (detail.assessmentId != assessmentId.value || detail.resultVersion != result.version || detail.snapshotId != result.matching.snapshotId || detail.ruleVersion != result.matching.ruleVersion || detail.orderingVersion != result.matching.orderingVersion) throw new Error('产品与本报告的冻结版本不一致,请重新选择产品。')
|
if (detail.assessmentId != assessmentId.value || detail.revisionId != result.revisionId || detail.resultVersion != result.version || detail.snapshotId != result.matching.snapshotId || detail.ruleVersion != result.matching.ruleVersion || detail.orderingVersion != result.matching.orderingVersion) throw new Error('产品与本报告的冻结版本不一致,请重新选择产品。')
|
||||||
match.value = detail.item
|
match.value = detail.item
|
||||||
} catch (err) { if (!stale(request, token)) matchError.value = err instanceof Error ? err.message : '产品读取失败,请重试' }
|
} catch (err) { if (!stale(request, token)) matchError.value = err instanceof Error ? err.message : '产品读取失败,请重试' }
|
||||||
}
|
}
|
||||||
@@ -110,12 +109,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const ready = (): boolean => !stale(generation, loadedToken) && report.value != null && !loading.value
|
const ready = (): boolean => !stale(generation, loadedToken) && report.value != null && !loading.value
|
||||||
const openReport = () => { if (ready()) uni.navigateTo({ url: '/packages/finance/report/report?assessmentId=' + assessmentId.value }) }
|
const openReport = () => { if (ready()) uni.navigateTo({ url: '/packages/finance/report/report?assessmentId=' + assessmentId.value + (revisionId.value.length > 0 ? '&revisionId=' + revisionId.value : '') }) }
|
||||||
const chooseProduct = () => { if (ready() && report.value!.matching.status == 'completed') uni.navigateTo({ url: '/packages/finance/matches/matches?assessmentId=' + assessmentId.value + '&group=eligible' }) }
|
const chooseProduct = () => { if (ready() && report.value!.matching.status == 'completed') uni.navigateTo({ url: '/packages/finance/matches/matches?assessmentId=' + assessmentId.value + (revisionId.value.length > 0 ? '&revisionId=' + revisionId.value : '') + '&group=eligible' }) }
|
||||||
const prepare = () => {
|
const prepare = () => {
|
||||||
if (!ready()) return
|
if (!ready()) return
|
||||||
if (section.value == 'prepare') { load(); return }
|
if (section.value == 'prepare') { load(); return }
|
||||||
uni.redirectTo({ url: nextStepRoute(assessmentId.value, productId.value, 'prepare') })
|
uni.redirectTo({ url: nextStepRoute(assessmentId.value, productId.value, 'prepare', revisionId.value) })
|
||||||
}
|
}
|
||||||
const optimize = () => {
|
const optimize = () => {
|
||||||
if (!ready()) return
|
if (!ready()) return
|
||||||
@@ -126,21 +125,23 @@
|
|||||||
const consult = () => {
|
const consult = () => {
|
||||||
if (!ready()) return
|
if (!ready()) return
|
||||||
if (productId.value.length > 0 && match.value == null) { uni.showToast({ title: '请先重新核对产品或从报告咨询AI', icon: 'none' }); return }
|
if (productId.value.length > 0 && match.value == null) { uni.showToast({ title: '请先重新核对产品或从报告咨询AI', icon: 'none' }); return }
|
||||||
uni.navigateTo({ url: assistantRoute(assessmentId.value, productId.value.length > 0 ? [productId.value] : [], 'next-step') })
|
uni.navigateTo({ url: assistantRoute(assessmentId.value, productId.value.length > 0 ? [productId.value] : [], 'next-step', '', revisionId.value) })
|
||||||
}
|
}
|
||||||
const restart = () => {
|
const restart = () => {
|
||||||
if (!ready() || !improveOpen.value) return
|
if (!ready() || !improveOpen.value) return
|
||||||
clearAssessmentFlow()
|
clearAssessmentFlow()
|
||||||
uni.navigateTo({ url: '/packages/finance/assessment/assessment?type=' + assessmentType.value })
|
uni.navigateTo({ url: '/packages/finance/assessment/assessment?editId=' + assessmentId.value })
|
||||||
}
|
}
|
||||||
const back = () => {
|
const back = () => {
|
||||||
if (getCurrentPages().length > 1) uni.navigateBack()
|
if (getCurrentPages().length > 1) uni.navigateBack()
|
||||||
else if (validRecordId(assessmentId.value)) uni.redirectTo({ url: '/packages/finance/report/report?assessmentId=' + assessmentId.value })
|
else if (validRecordId(assessmentId.value)) uni.redirectTo({ url: '/packages/finance/report/report?assessmentId=' + assessmentId.value + (revisionId.value.length > 0 ? '&revisionId=' + revisionId.value : '') })
|
||||||
else history()
|
else history()
|
||||||
}
|
}
|
||||||
onLoad((options) => {
|
onLoad((options) => {
|
||||||
|
revisionId.value = options?.['revisionId'] ?? '';
|
||||||
|
if (revisionId.value.length > 0 && !validRecordId(revisionId.value)) routeError.value = '版本编号无效,请返回重新选择。'
|
||||||
assessmentId.value = options?.['assessmentId'] ?? ''; productId.value = options?.['productId'] ?? ''; section.value = options?.['section'] ?? 'actions'
|
assessmentId.value = options?.['assessmentId'] ?? ''; productId.value = options?.['productId'] ?? ''; section.value = options?.['section'] ?? 'actions'
|
||||||
if (nextStepRoute(assessmentId.value, productId.value, section.value).length == 0) routeError.value = '页面参数无效,请从本次报告或匹配产品重新进入。'
|
if (nextStepRoute(assessmentId.value, productId.value, section.value, revisionId.value).length == 0) routeError.value = '页面参数无效,请从本次报告或匹配产品重新进入。'
|
||||||
})
|
})
|
||||||
onShow(() => { visible = true; load() })
|
onShow(() => { visible = true; load() })
|
||||||
onHide(() => { visible = false; generation++; reset(); loading.value = false; error.value = '' })
|
onHide(() => { visible = false; generation++; reset(); loading.value = false; error.value = '' })
|
||||||
|
|||||||
@@ -14,7 +14,6 @@
|
|||||||
<text class="product-name">{{ product!.name }}</text>
|
<text class="product-name">{{ product!.name }}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<text v-if="product!.demo" class="demo">演示数据 · 非真实金融产品</text>
|
|
||||||
<view class="tabs">
|
<view class="tabs">
|
||||||
<view v-if="assessmentId.length > 0" :class="tab == 'explanation' ? 'tab active' : 'tab'" @click="switchTab('explanation')"><text :class="tab == 'explanation' ? 'tab-label selected' : 'tab-label'">AI解释</text></view>
|
<view v-if="assessmentId.length > 0" :class="tab == 'explanation' ? 'tab active' : 'tab'" @click="switchTab('explanation')"><text :class="tab == 'explanation' ? 'tab-label selected' : 'tab-label'">AI解释</text></view>
|
||||||
<view :class="tab == 'details' ? 'tab active' : 'tab'" @click="switchTab('details')"><text :class="tab == 'details' ? 'tab-label selected' : 'tab-label'">产品详情</text></view>
|
<view :class="tab == 'details' ? 'tab active' : 'tab'" @click="switchTab('details')"><text :class="tab == 'details' ? 'tab-label selected' : 'tab-label'">产品详情</text></view>
|
||||||
@@ -47,7 +46,8 @@
|
|||||||
<text v-else-if="match!.aiStatus == 'failed'" class="ai-note">解释生成未完成,规则依据不受影响。{{ match!.aiCanRetry ? '可以重新尝试。' : '本次已达到重试上限。' }}</text>
|
<text v-else-if="match!.aiStatus == 'failed'" class="ai-note">解释生成未完成,规则依据不受影响。{{ match!.aiCanRetry ? '可以重新尝试。' : '本次已达到重试上限。' }}</text>
|
||||||
<text v-else class="ai-note">当前为规则依据。可让AI逐条说明条件含义和资料核对建议。</text>
|
<text v-else class="ai-note">当前为规则依据。可让AI逐条说明条件含义和资料核对建议。</text>
|
||||||
<text v-if="aiError.length > 0" class="attention-text ai-note">{{ aiError }}</text>
|
<text v-if="aiError.length > 0" class="attention-text ai-note">{{ aiError }}</text>
|
||||||
<button v-if="!aiPending && match!.aiStatus != 'completed' && (match!.aiStatus != 'failed' || match!.aiCanRetry)" class="ai-button" :disabled="aiBusy" @click="generateExplanation">{{ aiBusy ? '正在提交…' : (match!.aiStatus == 'failed' ? '重试AI解释' : '生成AI详细解释') }}</button>
|
<text v-if="!match!.aiCanRequest" class="ai-note">历史版本保留已保存的规则与解释。</text>
|
||||||
|
<button v-if="match!.aiCanRequest && !aiPending && match!.aiStatus != 'completed' && (match!.aiStatus != 'failed' || match!.aiCanRetry)" class="ai-button" :disabled="aiBusy" @click="generateExplanation">{{ aiBusy ? '正在提交…' : (match!.aiStatus == 'failed' ? '重试AI解释' : '生成AI详细解释') }}</button>
|
||||||
<button v-if="aiPending && (aiError.length > 0 || pollStopped)" class="ai-button" :disabled="aiBusy" @click="checkExplanation">重新查询状态</button>
|
<button v-if="aiPending && (aiError.length > 0 || pollStopped)" class="ai-button" :disabled="aiBusy" @click="checkExplanation">重新查询状态</button>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
@@ -84,11 +84,11 @@
|
|||||||
import AppHeader from '@/components/AppHeader.uvue'
|
import AppHeader from '@/components/AppHeader.uvue'
|
||||||
import { usePageLayout } from '@/services/layout.uts'
|
import { usePageLayout } from '@/services/layout.uts'
|
||||||
import { getSession } from '@/services/api.uts'
|
import { getSession } from '@/services/api.uts'
|
||||||
import { MatchView, MatchReason, ProductView, fetchMatchDetail, fetchPublicProduct, validRecordId, requestProductExplanation } from '@/packages/finance/services/matches.uts'
|
import { MatchView, MatchReason, ProductView, fetchMatchDetailRecord, fetchPublicProduct, validRecordId, requestProductExplanation } from '@/packages/finance/services/matches.uts'
|
||||||
import { parseCompareIds, compareSelection, saveCompareSelection, compareQuery } from '@/packages/finance/services/compare.uts'
|
import { parseCompareIds, compareSelection, saveCompareSelection, compareQuery } from '@/packages/finance/services/compare.uts'
|
||||||
import { assistantRoute } from '@/packages/finance/services/assistant.uts'
|
import { assistantRoute } from '@/packages/finance/services/assistant.uts'
|
||||||
const { pageStyle, contentStyle, bottomStyle } = usePageLayout()
|
const { pageStyle, contentStyle, bottomStyle } = usePageLayout()
|
||||||
const assessmentId = ref(''); const productId = ref(''); const tab = ref('details')
|
const revisionId = ref(''); const assessmentId = ref(''); const productId = ref(''); const tab = ref('details')
|
||||||
const selectedIds = ref<string[]>([]); let from = ''
|
const selectedIds = ref<string[]>([]); let from = ''
|
||||||
const product = ref<ProductView | null>(null); const match = ref<MatchView | null>(null)
|
const product = ref<ProductView | null>(null); const match = ref<MatchView | null>(null)
|
||||||
const loading = ref(false); const error = ref(''); const expanded = ref(false)
|
const loading = ref(false); const error = ref(''); const expanded = ref(false)
|
||||||
@@ -122,8 +122,9 @@
|
|||||||
const request = generation; const token = getSession()?.getString('token') ?? ''
|
const request = generation; const token = getSession()?.getString('token') ?? ''
|
||||||
aiError.value = ''; aiBusy.value = true
|
aiError.value = ''; aiBusy.value = true
|
||||||
try {
|
try {
|
||||||
const result = await fetchMatchDetail(assessmentId.value, productId.value)
|
const detail = await fetchMatchDetailRecord(assessmentId.value, productId.value, revisionId.value)
|
||||||
if (current(request, token)) { match.value = result; schedulePoll() }
|
const result = detail.item
|
||||||
|
if (current(request, token)) { revisionId.value = detail.revisionId; match.value = result; schedulePoll() }
|
||||||
} catch (err) { explanationError(request, token, err instanceof Error ? err.message : '解释状态读取失败,请重试')
|
} catch (err) { explanationError(request, token, err instanceof Error ? err.message : '解释状态读取失败,请重试')
|
||||||
} finally { if (current(request, token)) aiBusy.value = false }
|
} finally { if (current(request, token)) aiBusy.value = false }
|
||||||
}
|
}
|
||||||
@@ -136,7 +137,7 @@
|
|||||||
if (requestKey.length == 0) requestKey = 'explain_' + Date.now().toString() + '_' + Math.random().toString(36).slice(2, 12)
|
if (requestKey.length == 0) requestKey = 'explain_' + Date.now().toString() + '_' + Math.random().toString(36).slice(2, 12)
|
||||||
aiBusy.value = true; aiError.value = ''; pollCount = 0; pollStopped.value = false
|
aiBusy.value = true; aiError.value = ''; pollCount = 0; pollStopped.value = false
|
||||||
try {
|
try {
|
||||||
const result = await requestProductExplanation(assessmentId.value, productId.value, requestKey)
|
const result = await requestProductExplanation(assessmentId.value, productId.value, requestKey, revisionId.value)
|
||||||
if (current(request, token)) { match.value = result; requestKey = ''; schedulePoll() }
|
if (current(request, token)) { match.value = result; requestKey = ''; schedulePoll() }
|
||||||
} catch (err) { explanationError(request, token, err instanceof Error ? err.message : 'AI解释请求失败,请重试')
|
} catch (err) { explanationError(request, token, err instanceof Error ? err.message : 'AI解释请求失败,请重试')
|
||||||
} finally { if (current(request, token)) aiBusy.value = false }
|
} finally { if (current(request, token)) aiBusy.value = false }
|
||||||
@@ -151,38 +152,41 @@
|
|||||||
try {
|
try {
|
||||||
// 带评估编号始终读取冻结结果;404/越权/失败不能降级为公共资料。
|
// 带评估编号始终读取冻结结果;404/越权/失败不能降级为公共资料。
|
||||||
if (assessmentId.value.length > 0) {
|
if (assessmentId.value.length > 0) {
|
||||||
const result = await fetchMatchDetail(assessmentId.value, productId.value)
|
const detail = await fetchMatchDetailRecord(assessmentId.value, productId.value, revisionId.value)
|
||||||
if (visible && generation == request && getSession()?.getString('token') == token) { match.value = result; product.value = result.product; schedulePoll() }
|
const result = detail.item
|
||||||
|
if (current(request, token)) { revisionId.value = detail.revisionId; match.value = result; product.value = result.product; schedulePoll() }
|
||||||
} else {
|
} else {
|
||||||
const result = await fetchPublicProduct(productId.value)
|
const result = await fetchPublicProduct(productId.value)
|
||||||
if (visible && generation == request && getSession()?.getString('token') == token) product.value = result
|
if (current(request, token)) product.value = result
|
||||||
}
|
}
|
||||||
} catch (err) { if (visible && generation == request) error.value = err instanceof Error ? err.message : '产品读取失败' }
|
} catch (err) { if (visible && generation == request) error.value = err instanceof Error ? err.message : '产品读取失败' }
|
||||||
finally { if (generation == request) loading.value = false }
|
finally { if (generation == request) loading.value = false }
|
||||||
}
|
}
|
||||||
const history = () => { uni.navigateTo({ url: '/packages/finance/archive/archive' }) }
|
const history = () => { uni.navigateTo({ url: '/packages/finance/archive/archive' }) }
|
||||||
// 仅携带选择ID;助手会重新校验归属并读取冻结事实,进入页面不会调用模型。
|
// 仅携带选择ID;助手会重新校验归属并读取冻结事实,进入页面不会调用模型。
|
||||||
const consult = () => { uni.navigateTo({ url: assistantRoute(assessmentId.value, [productId.value], 'product') }) }
|
const consult = () => { uni.navigateTo({ url: assistantRoute(assessmentId.value, [productId.value], 'product', '', revisionId.value) }) }
|
||||||
function toMatches(): void { if (assessmentId.value.length > 0) uni.redirectTo({ url: '/packages/finance/matches/matches' + compareQuery(assessmentId.value, selectedIds.value) }); else history() }
|
function toMatches(): void { if (assessmentId.value.length > 0) uni.redirectTo({ url: '/packages/finance/matches/matches' + compareQuery(assessmentId.value, selectedIds.value, revisionId.value) }); else history() }
|
||||||
// 替换当前路由保留栏目、来源与对比选择,刷新后仍停留在同一栏。
|
// 替换当前路由保留栏目、来源与对比选择,刷新后仍停留在同一栏。
|
||||||
const switchTab = (value: string) => {
|
const switchTab = (value: string) => {
|
||||||
if (value == tab.value) return
|
if (value == tab.value) return
|
||||||
const query = assessmentId.value.length > 0 ? compareQuery(assessmentId.value, selectedIds.value) + '&' : '?'
|
const query = assessmentId.value.length > 0 ? compareQuery(assessmentId.value, selectedIds.value, revisionId.value) + '&' : '?'
|
||||||
uni.redirectTo({ url: '/packages/finance/product/product' + query + 'productId=' + productId.value + '&tab=' + value + '&from=' + from })
|
uni.redirectTo({ url: '/packages/finance/product/product' + query + 'productId=' + productId.value + '&tab=' + value + '&from=' + from })
|
||||||
}
|
}
|
||||||
// 下一步页重新核对当前可用性,避免沿用产品详情进入时的旧状态。
|
// 下一步页重新核对当前可用性,避免沿用产品详情进入时的旧状态。
|
||||||
const prepare = () => { if (canPrepare.value) uni.navigateTo({ url: '/packages/finance/next-step/next-step?assessmentId=' + assessmentId.value + '&productId=' + productId.value + '§ion=prepare' }) }
|
const prepare = () => { if (canPrepare.value) uni.navigateTo({ url: '/packages/finance/next-step/next-step?assessmentId=' + assessmentId.value + (revisionId.value.length > 0 ? '&revisionId=' + revisionId.value : '') + '&productId=' + productId.value + '§ion=prepare' }) }
|
||||||
const back = () => { if (getCurrentPages().length > 1) uni.navigateBack(); else if (from == 'compare') uni.redirectTo({ url: '/packages/finance/compare/compare' + compareQuery(assessmentId.value, selectedIds.value) }); else toMatches() }
|
const back = () => { if (getCurrentPages().length > 1) uni.navigateBack(); else if (from == 'compare') uni.redirectTo({ url: '/packages/finance/compare/compare' + compareQuery(assessmentId.value, selectedIds.value, revisionId.value) }); else toMatches() }
|
||||||
const openCompare = () => {
|
const openCompare = () => {
|
||||||
if (match.value == null) return
|
if (match.value == null) return
|
||||||
if (!selectedIds.value.includes(productId.value)) {
|
if (!selectedIds.value.includes(productId.value)) {
|
||||||
if (selectedIds.value.length >= 3) { uni.showToast({ title: '已选满3项,请在对比页调整选择', icon: 'none' }); return }
|
if (selectedIds.value.length >= 3) { uni.showToast({ title: '已选满3项,请在对比页调整选择', icon: 'none' }); return }
|
||||||
selectedIds.value.push(productId.value)
|
selectedIds.value.push(productId.value)
|
||||||
}
|
}
|
||||||
saveCompareSelection(assessmentId.value, selectedIds.value)
|
saveCompareSelection(assessmentId.value, selectedIds.value, revisionId.value)
|
||||||
uni.navigateTo({ url: '/packages/finance/compare/compare' + compareQuery(assessmentId.value, selectedIds.value) })
|
uni.navigateTo({ url: '/packages/finance/compare/compare' + compareQuery(assessmentId.value, selectedIds.value, revisionId.value) })
|
||||||
}
|
}
|
||||||
onLoad((options) => {
|
onLoad((options) => {
|
||||||
|
revisionId.value = options?.['revisionId'] ?? '';
|
||||||
|
if (revisionId.value.length > 0 && !validRecordId(revisionId.value)) routeError = '版本编号无效,请返回重新选择。'
|
||||||
assessmentId.value = options?.['assessmentId'] ?? ''; const oldId = options?.['id'] ?? ''
|
assessmentId.value = options?.['assessmentId'] ?? ''; const oldId = options?.['id'] ?? ''
|
||||||
productId.value = options?.['productId'] ?? oldId
|
productId.value = options?.['productId'] ?? oldId
|
||||||
if (options?.['assessmentId'] != null && !validRecordId(assessmentId.value)) routeError = '评估编号无效,请从报告重新进入。'
|
if (options?.['assessmentId'] != null && !validRecordId(assessmentId.value)) routeError = '评估编号无效,请从报告重新进入。'
|
||||||
@@ -192,11 +196,11 @@
|
|||||||
from = options?.['from'] ?? ''
|
from = options?.['from'] ?? ''
|
||||||
if (!['','matches','product','compare','report','next-step','archive'].includes(from)) routeError = '页面来源参数无效,请重新选择。'
|
if (!['','matches','product','compare','report','next-step','archive'].includes(from)) routeError = '页面来源参数无效,请重新选择。'
|
||||||
if (options?.['productIds'] != null) {
|
if (options?.['productIds'] != null) {
|
||||||
try { saveCompareSelection(assessmentId.value, parseCompareIds(options!['productIds']!)) }
|
try { saveCompareSelection(assessmentId.value, parseCompareIds(options!['productIds']!), revisionId.value) }
|
||||||
catch (err) { routeError = err instanceof Error ? err.message : '对比参数无效' }
|
catch (err) { routeError = err instanceof Error ? err.message : '对比参数无效' }
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
onShow(() => { visible = true; pageActive.value = true; selectedIds.value = compareSelection(assessmentId.value); load() })
|
onShow(() => { visible = true; pageActive.value = true; selectedIds.value = compareSelection(assessmentId.value, revisionId.value); load() })
|
||||||
onHide(() => { pageActive.value = false; visible = false; stopPoll(); generation++; product.value = null; match.value = null; error.value = ''; aiError.value = ''; aiBusy.value = false; loading.value = false; openReason.value = '' })
|
onHide(() => { pageActive.value = false; visible = false; stopPoll(); generation++; product.value = null; match.value = null; error.value = ''; aiError.value = ''; aiBusy.value = false; loading.value = false; openReason.value = '' })
|
||||||
onUnload(() => { pageActive.value = false; visible = false; stopPoll(); generation++; product.value = null; match.value = null; requestKey = '' })
|
onUnload(() => { pageActive.value = false; visible = false; stopPoll(); generation++; product.value = null; match.value = null; requestKey = '' })
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,34 +1,38 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="page" :style="pageStyle()">
|
<view class="page" :style="pageStyle()">
|
||||||
<AppHeader title="我的融资报告" :customBack="true" @back="back" />
|
<view class="status-bar" :style="statusStyle(0)"></view>
|
||||||
|
<view class="report-header">
|
||||||
|
<view class="header-back" @click="back"><AppIcon name="chevron-right" :size="40" class="back-icon" /></view>
|
||||||
|
<view class="header-title-area" :style="reportTitleStyle()"><text class="header-title">我的融资报告</text></view>
|
||||||
|
<view v-if="report != null" class="header-export" :style="navigationActionStyle(96, 1)"><ReportExport :assessmentId="assessmentId" :revisionId="revisionId" :toolbar="true" @authrequired="login" @sessionchanged="loadReport" @statuschange="exportStatus" /></view>
|
||||||
|
</view>
|
||||||
<view class="tabs"><view v-for="item in tabs" :key="item.id" :class="tab == item.id ? 'tab active' : 'tab'" @click="switchTab(item.id)"><text :class="tab == item.id ? 'tab-label selected' : 'tab-label'">{{ item.label }}</text></view></view>
|
<view class="tabs"><view v-for="item in tabs" :key="item.id" :class="tab == item.id ? 'tab active' : 'tab'" @click="switchTab(item.id)"><text :class="tab == item.id ? 'tab-label selected' : 'tab-label'">{{ item.label }}</text></view></view>
|
||||||
<scroll-view scroll-y :style="contentStyle(186, false)">
|
<scroll-view scroll-y :style="contentStyle(report == null ? 178 : 314, false)">
|
||||||
<view class="report-content">
|
<view class="report-content" :style="bottomStyle(24)">
|
||||||
<!-- 离页清空报告时只切换可见性,避免在页面销毁过程中重新挂载文本节点。 -->
|
<!-- 离页清空报告时只切换可见性,避免在页面销毁过程中重新挂载文本节点。 -->
|
||||||
<text v-show="report == null" class="eyebrow">评估记录 #{{ assessmentId.length > 0 ? assessmentId : '待选择' }}</text>
|
<text v-show="report == null" class="eyebrow">评估记录 #{{ assessmentId.length > 0 ? assessmentId : '待选择' }}</text>
|
||||||
<text v-if="loading" class="body-text">正在读取已保存报告…</text>
|
<text v-if="loading" class="body-text">正在读取已保存报告…</text>
|
||||||
<view v-if="error.length > 0" class="panel"><text class="section-title">报告暂不可读取</text><text class="body-text">{{ error }}</text><button v-if="routeError.length == 0" class="secondary" @click="loadReport">重新加载报告</button><text class="action" @click="history">前往融资档案</text></view>
|
<view v-if="error.length > 0" class="panel"><text class="section-title">报告暂不可读取</text><text class="body-text">{{ error }}</text><button v-if="routeError.length == 0" class="secondary" @click="loadReport">重新加载报告</button><text class="action" @click="history">前往融资档案</text></view>
|
||||||
<template v-if="report != null">
|
<template v-if="report != null">
|
||||||
<ReportExport :assessmentId="assessmentId" :compact="true" @authrequired="login" @sessionchanged="loadReport" />
|
<text v-if="report!.isPreviousResult" class="revision-notice">当前查看第 {{ report!.revisionNo }} 版结果。最新提交{{ report!.currentStatus == 'processing' ? '正在评估,成功后更新结果。' : (report!.currentStatus == 'failed' ? '评估失败,已保留上次成功结果。' : '已有新版本,可从档案查看。') }}</text>
|
||||||
<text class="metadata-toggle" @click="metadataOpen = !metadataOpen">用户自填资料分析 · 报告信息{{ metadataOpen ? '收起' : '展开' }}</text>
|
<text v-show="downloadStatus.length > 0" class="download-status">{{ downloadStatus }}</text>
|
||||||
<view v-if="metadataOpen" class="metadata"><text class="report-note">生成于 {{ report!.generatedAt }}</text><text class="report-note">报告已保存至融资档案。导出不含身份信息,请妥善保管下载文件。</text></view>
|
|
||||||
<view v-if="report!.version == 1" class="legacy"><text class="report-note">这是一份旧版自填报告,当时未采集融资需求、未计算完整度与准备状态,也未执行产品匹配。历史内容保持原样。</text></view>
|
<view v-if="report!.version == 1" class="legacy"><text class="report-note">这是一份旧版自填报告,当时未采集融资需求、未计算完整度与准备状态,也未执行产品匹配。历史内容保持原样。</text></view>
|
||||||
<template v-if="tab == 'overview'">
|
<template v-if="tab == 'overview'">
|
||||||
<view class="hero">
|
<view :class="report!.readinessLabel == '可进入资料核对' ? 'readiness-band ready' : 'readiness-band attention'"><text class="readiness-label">融资准备状态</text><text class="readiness-title">{{ report!.readinessLabel }}</text></view>
|
||||||
<text class="hero-label">本次融资准备状态</text><text class="hero-title">{{ report!.readinessLabel }}</text>
|
<text class="summary">{{ report!.summary }}</text>
|
||||||
<text class="hero-note">{{ report!.version == 1 ? '旧版未评定,可重新填写并发起新评估' : '按本次提交资料及评估时规则生成' }}</text>
|
<view class="priority-section"><text class="section-title">{{ priorityItems.length > 0 ? '需重点完善的 ' + priorityItems.slice(0, 3).length.toString() + ' 项' : '资料核验与准备' }}</text><view v-for="(item, index) in priorityItems.slice(0, 3)" :key="item" class="priority-row"><text class="priority-number">{{ index + 1 }}</text><text class="priority-text">{{ item }}</text></view><text v-if="priorityItems.length == 0" class="report-note">本次暂无额外改善项,请按机构要求核实资料。</text><text v-if="priorityItems.length > 3" class="more-advice" @click="switchTab('improvements')">查看全部 {{ priorityItems.length }} 项改善建议 ›</text></view>
|
||||||
<view class="hero-divider"></view><view class="panel-heading"><text class="hero-label">资料完整度</text><text class="percent">{{ report!.completenessPercent == null ? '—' : report!.completenessPercent!.toString() + '%' }}</text></view>
|
<!-- 首屏保持原型的状态、摘要和编号改善项;完整度与所有冻结依据按需展开。 -->
|
||||||
<view v-if="report!.completenessPercent != null" class="progress-track"><view class="progress-fill" :style="'width:' + report!.completenessPercent!.toString() + '%;'"></view></view>
|
<view class="details-toggle" @click="detailsOpen = !detailsOpen"><text class="details-label">查看完整报告</text><text class="details-action">{{ detailsOpen ? '收起' : '展开' }}</text></view>
|
||||||
<text class="hero-note">{{ report!.completenessText }}</text><text class="hero-note">完整度不代表信用评分、资料真实性或获批概率。</text>
|
<template v-if="detailsOpen">
|
||||||
</view>
|
<view v-if="report!.suggestions.length > 0" class="panel"><text class="section-title">改善建议与依据</text><text v-for="item in report!.suggestions" :key="item" class="body-text">{{ item }}</text></view>
|
||||||
<view v-if="report!.version == 2" class="panel"><text class="section-title">本次资金需求</text><view class="fact-row" v-for="fact in report!.demand" :key="fact.label"><text class="fact-label">{{ fact.label }}</text><text class="fact-value">{{ fact.value }}</text></view></view>
|
<view class="panel"><view class="panel-heading"><text class="section-title">资料完整度</text><text class="percent">{{ report!.completenessPercent == null ? '—' : report!.completenessPercent!.toString() + '%' }}</text></view><view v-if="report!.completenessPercent != null" class="progress-track"><view class="progress-fill" :style="'width:' + report!.completenessPercent!.toString() + '%;'"></view></view><text class="report-note">{{ report!.completenessText }}</text><text class="report-note">完整度不代表信用评分、资料真实性或获批概率。</text></view>
|
||||||
<view class="panel"><text class="section-title">分析摘要</text><text class="body-text">{{ report!.summary }}</text><text class="source-note">分析来源:已保存的AI文本 · {{ report!.modelCode }}</text></view>
|
<view v-if="report!.demand.length > 0" class="panel"><text class="section-title">本次资金需求</text><view class="fact-row" v-for="fact in report!.demand" :key="fact.label"><text class="fact-label">{{ fact.label }}</text><text class="fact-value">{{ fact.value }}</text></view></view>
|
||||||
<view v-if="report!.version == 2" class="panel"><text class="section-title">准备状态依据</text><text v-if="report!.readinessBasis.length == 0" class="report-note">本次没有可展示的准备状态依据。</text><text class="body-text" v-for="(item, index) in report!.readinessBasis" :key="index">{{ item }}</text><text class="report-note">资料未经核验;完整度达到100%仍需核对机构要求。</text></view>
|
<view v-if="report!.version == 2" class="panel"><text class="section-title">准备状态依据</text><text v-if="report!.readinessBasis.length == 0" class="report-note">本次没有可展示的准备状态依据。</text><text class="body-text" v-for="(item, index) in report!.readinessBasis" :key="index">{{ item }}</text><text class="report-note">资料未经核验;完整度达到100%仍需核对机构要求。</text></view>
|
||||||
<view class="panel"><text class="section-title">{{ report!.version == 2 ? '主要优势' : '资料解读' }}</text><text v-if="report!.observations.length == 0" class="report-note">暂无有充分依据的优势说明。</text><text class="body-text" v-for="(item, index) in report!.observations" :key="index">{{ item }}</text></view>
|
<view class="panel"><text class="section-title">{{ report!.version == 2 ? '主要优势' : '资料解读' }}</text><text v-if="report!.observations.length == 0" class="report-note">暂无有充分依据的优势说明。</text><text class="body-text" v-for="(item, index) in report!.observations" :key="index">{{ item }}</text></view>
|
||||||
<view class="panel"><view class="panel-heading" @click="factsOpen = !factsOpen"><text class="section-title">本次提交的事实</text><text class="toggle">{{ factsOpen ? '收起' : '展开' }}</text></view><text class="report-note">来源:用户自填,未经外部核验;未填写不等于零。</text><template v-if="factsOpen"><view class="fact-row" v-for="fact in report!.facts" :key="fact.label"><text class="fact-label">{{ fact.label }}</text><text class="fact-value">{{ fact.value }}</text></view><text v-if="report!.facts.length == 0" class="report-note">暂无可展示的事实。</text></template></view>
|
<view class="panel"><view class="panel-heading" @click="factsOpen = !factsOpen"><text class="section-title">本次提交的事实</text><text class="toggle">{{ factsOpen ? '收起' : '展开' }}</text></view><text class="report-note">来源:用户自填,未经外部核验;未填写不等于零。</text><template v-if="factsOpen"><view class="fact-row" v-for="fact in report!.facts" :key="fact.label"><text class="fact-label">{{ fact.label }}</text><text class="fact-value">{{ fact.value }}</text></view><text v-if="report!.facts.length == 0" class="report-note">暂无可展示的事实。</text></template></view>
|
||||||
<view v-if="report!.version == 2" class="panel"><view class="panel-heading" @click="completenessOpen = !completenessOpen"><text class="section-title">逐项填写情况</text><text class="toggle">{{ completenessOpen ? '收起' : '展开' }}</text></view><template v-if="completenessOpen"><view class="fact-row" v-for="item in report!.completenessItems" :key="item.label"><text class="fact-label">{{ item.label }}</text><text class="fact-value">{{ item.value }}</text></view><text v-if="report!.completenessItems.length == 0" class="report-note">本次未评定逐项完整度。</text></template></view>
|
<view v-if="report!.version == 2" class="panel"><view class="panel-heading" @click="completenessOpen = !completenessOpen"><text class="section-title">逐项填写情况</text><text class="toggle">{{ completenessOpen ? '收起' : '展开' }}</text></view><template v-if="completenessOpen"><view class="fact-row" v-for="item in report!.completenessItems" :key="item.label"><text class="fact-label">{{ item.label }}</text><text class="fact-value">{{ item.value }}</text></view><text v-if="report!.completenessItems.length == 0" class="report-note">本次未评定逐项完整度。</text></template></view>
|
||||||
<view class="panel"><text class="section-title">{{ report!.version == 2 ? '风险与核验边界' : '待补充信息' }}</text><text class="body-text" v-for="(item, index) in report!.warnings" :key="'system-' + index">{{ item }}</text><text class="body-text" v-for="(item, index) in report!.dataGaps" :key="index">{{ item }}</text><text v-if="report!.warnings.length == 0 && report!.dataGaps.length == 0" class="report-note">本次暂无额外提示,不代表资料已核验或无风险。</text></view>
|
<view class="panel"><text class="section-title">{{ report!.version == 2 ? '风险与核验边界' : '待补充信息' }}</text><text class="body-text" v-for="(item, index) in report!.warnings" :key="'system-' + index">{{ item }}</text><text class="body-text" v-for="(item, index) in report!.dataGaps" :key="index">{{ item }}</text><text v-if="report!.warnings.length == 0 && report!.dataGaps.length == 0" class="report-note">本次暂无额外提示,不代表资料已核验或无风险。</text></view>
|
||||||
<button class="primary" @click="switchTab('matches')">查看本次匹配与依据</button>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="tab == 'matches'">
|
<template v-else-if="tab == 'matches'">
|
||||||
<text class="headline">本次需求的可选方向</text><text class="intro">以下为评估时的条件判断。展示顺序不代表优劣,也不代表机构审批。</text>
|
<text class="headline">本次需求的可选方向</text><text class="intro">以下为评估时的条件判断。展示顺序不代表优劣,也不代表机构审批。</text>
|
||||||
@@ -39,29 +43,31 @@
|
|||||||
<text v-if="candidateLoading" class="body-text">正在读取历史候选…</text>
|
<text v-if="candidateLoading" class="body-text">正在读取历史候选…</text>
|
||||||
<view v-if="candidateError.length > 0" class="panel"><text class="section-title">候选读取失败</text><text class="body-text">{{ candidateError }}</text><button class="secondary" @click="loadCandidates">重试候选</button></view>
|
<view v-if="candidateError.length > 0" class="panel"><text class="section-title">候选读取失败</text><text class="body-text">{{ candidateError }}</text><button class="secondary" @click="loadCandidates">重试候选</button></view>
|
||||||
<view v-if="!candidateLoading && candidateError.length == 0 && candidates.length == 0" class="panel"><text class="section-title">暂无符合已知条件的产品</text><text class="body-text">本次已执行匹配,可查看条件不符或待核实原因。没有候选不代表永久无法融资。</text></view>
|
<view v-if="!candidateLoading && candidateError.length == 0 && candidates.length == 0" class="panel"><text class="section-title">暂无符合已知条件的产品</text><text class="body-text">本次已执行匹配,可查看条件不符或待核实原因。没有候选不代表永久无法融资。</text></view>
|
||||||
<view v-for="item in candidates" :key="item.id" class="product-card" @click="openProduct(item.id)"><view class="panel-heading"><text class="product-name">{{ item.product.name }}</text><text class="version">v{{ item.product.version }}</text></view><text class="report-note">{{ item.product.bank }} · {{ item.stateLabel }}</text><text v-if="item.product.demo" class="demo">演示产品 · 非真实金融产品</text><text class="amount">{{ item.product.amount }}</text><view class="metrics"><text class="metric">年化 {{ item.product.rate }}</text><text class="metric">{{ item.product.term }}</text></view><text class="reason" v-for="reason in item.positive.slice(0, 2)" :key="reason.id">{{ reason.text }}</text><text v-if="item.availability != 'available'" class="availability">{{ item.availabilityLabel }};仍保留历史参数。</text><text class="action">查看匹配依据与详情 ›</text></view>
|
<view v-for="item in candidates" :key="item.id" class="product-card" @click="openProduct(item.id)"><view class="panel-heading"><text class="product-name">{{ item.product.name }}</text><text class="version">v{{ item.product.version }}</text></view><text class="report-note">{{ item.product.bank }} · {{ item.stateLabel }}</text><text class="amount">{{ item.product.amount }}</text><view class="metrics"><text class="metric">年化 {{ item.product.rate }}</text><text class="metric">{{ item.product.term }}</text></view><text class="reason" v-for="reason in item.positive.slice(0, 2)" :key="reason.id">{{ reason.text }}</text><text v-if="item.availability != 'available'" class="availability">{{ item.availabilityLabel }};仍保留历史参数。</text><text class="action">查看匹配依据与详情 ›</text></view>
|
||||||
<text v-if="candidates.length > 0" class="report-note">摘要最多展示3项符合已知条件的产品,全部分组见完整匹配列表。</text>
|
<text v-if="candidates.length > 0" class="report-note">摘要最多展示3项符合已知条件的产品,全部分组见完整匹配列表。</text>
|
||||||
<button class="primary" @click="allMatches('eligible')">查看全部匹配结果</button><view class="links"><text class="action" @click="allMatches('ineligible')">查看不符原因</text><text class="action" @click="allMatches('unknown')">查看待核实项</text></view>
|
<button class="primary" @click="allMatches('eligible')">查看全部匹配结果</button><view class="links"><text class="action" @click="allMatches('ineligible')">查看不符原因</text><text class="action" @click="allMatches('unknown')">查看待核实项</text></view>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<text class="headline">让下一次准备更充分</text><text class="intro">依据本次报告整理 · 改善不保证获批</text>
|
<text class="headline">改善建议</text><text class="intro">依据本次报告整理 · 改善不保证获批</text>
|
||||||
|
<view class="priority-section"><text class="section-title">{{ priorityItems.length > 0 ? '需重点完善的 ' + priorityItems.length.toString() + ' 项' : '资料核验与准备' }}</text><view v-for="(item, index) in priorityItems" :key="item" class="priority-row"><text class="priority-number">{{ index + 1 }}</text><text class="priority-text">{{ item }}</text></view><text v-if="priorityItems.length == 0" class="report-note">本次暂无额外改善项,请按机构要求核实资料。</text></view>
|
||||||
|
<view v-if="report!.suggestions.length > 0" class="panel"><view class="panel-heading" @click="detailsOpen = !detailsOpen"><text class="section-title">建议依据</text><text class="toggle">{{ detailsOpen ? '收起' : '展开' }}</text></view><template v-if="detailsOpen"><text v-for="item in report!.suggestions" :key="item" class="body-text">{{ item }}</text></template></view>
|
||||||
<view v-if="report!.version == 2" class="panel"><text class="section-title">优先补充与核实</text><text v-if="missingItems.length == 0" class="report-note">{{ report!.completenessPercent == null ? '本次未评定缺失字段,不能据此认为资料齐全。' : '本次字段清单没有待补充或格式待核实项,仍需准备核验材料。' }}</text><view class="fact-row" v-for="item in missingItems" :key="item.label"><text class="fact-label">{{ item.label }}</text><text class="fact-value">{{ item.value }}</text></view><text class="body-text" v-for="(item, index) in report!.readinessBasis" :key="index">{{ item }}</text></view>
|
<view v-if="report!.version == 2" class="panel"><text class="section-title">优先补充与核实</text><text v-if="missingItems.length == 0" class="report-note">{{ report!.completenessPercent == null ? '本次未评定缺失字段,不能据此认为资料齐全。' : '本次字段清单没有待补充或格式待核实项,仍需准备核验材料。' }}</text><view class="fact-row" v-for="item in missingItems" :key="item.label"><text class="fact-label">{{ item.label }}</text><text class="fact-value">{{ item.value }}</text></view><text class="body-text" v-for="(item, index) in report!.readinessBasis" :key="index">{{ item }}</text></view>
|
||||||
<view class="panel"><text class="section-title">已保存的AI改善建议</text><text v-if="report!.suggestions.length == 0" class="report-note">暂无有依据的额外建议,不代表已经完成核验。</text><text class="body-text" v-for="(item, index) in report!.suggestions" :key="index">{{ item }}</text><text class="source-note">保留报告生成时的文本与依据;补充资料后需发起新评估。</text></view>
|
|
||||||
<view v-if="report!.matching.status != 'completed'" class="panel"><text class="section-title">产品条件提示</text><text class="report-note">本次未执行产品匹配,没有可关联的产品改善依据。</text></view>
|
<view v-if="report!.matching.status != 'completed'" class="panel"><text class="section-title">产品条件提示</text><text class="report-note">本次未执行产品匹配,没有可关联的产品改善依据。</text></view>
|
||||||
<template v-else><view v-for="group in adviceGroups" :key="group.id" class="panel"><text class="section-title">{{ group.label }}</text><text class="report-note">仅展示本组前3项产品的已有依据,可进入完整列表查看更多。</text><text v-if="group.loading" class="body-text">正在读取产品依据…</text><template v-if="group.error.length > 0"><text class="body-text">{{ group.error }}</text><button class="secondary" @click="loadAdvice(group.id)">重试{{ group.label }}</button></template><text v-if="!group.loading && group.error.length == 0 && group.items.length == 0" class="report-note">本组暂无产品依据。</text><view v-for="item in group.items" :key="item.id" class="advice-product"><text class="product-name" @click="openProduct(item.id)">{{ item.product.name }} ›</text><text class="source-note">{{ item.stateLabel }} · 评估时版本 v{{ item.product.version }}</text><view v-for="reason in adviceReasons(item)" :key="reason.id"><text class="body-text">{{ reason.text }}</text><text v-for="line in reason.evidence" :key="line" class="evidence">{{ line }}</text></view><text v-if="item.availability != 'available'" class="availability">{{ item.availabilityLabel }}</text></view><text class="action" @click="allMatches(group.id)">查看全部{{ group.label }} ›</text></view></template>
|
<template v-else><view v-for="group in adviceGroups" :key="group.id" class="panel"><text class="section-title">{{ group.label }}</text><text class="report-note">仅展示本组前3项产品的已有依据,可进入完整列表查看更多。</text><text v-if="group.loading" class="body-text">正在读取产品依据…</text><template v-if="group.error.length > 0"><text class="body-text">{{ group.error }}</text><button class="secondary" @click="loadAdvice(group.id)">重试{{ group.label }}</button></template><text v-if="!group.loading && group.error.length == 0 && group.items.length == 0" class="report-note">本组暂无产品依据。</text><view v-for="item in group.items" :key="item.id" class="advice-product"><text class="product-name" @click="openProduct(item.id)">{{ item.product.name }} ›</text><text class="source-note">{{ item.stateLabel }} · 评估时版本 v{{ item.product.version }}</text><view v-for="reason in adviceReasons(item)" :key="reason.id"><text class="body-text">{{ reason.text }}</text><text v-for="line in reason.evidence" :key="line" class="evidence">{{ line }}</text></view><text v-if="item.availability != 'available'" class="availability">{{ item.availabilityLabel }}</text></view><text class="action" @click="allMatches(group.id)">查看全部{{ group.label }} ›</text></view></template>
|
||||||
</template>
|
</template>
|
||||||
<text class="disclaimer">{{ report!.disclaimer }}</text>
|
<view class="report-links"><text class="report-link" @click="nextStep">下一步行动 ›</text><text class="report-link" @click="consult">咨询AI ›</text></view>
|
||||||
<button class="primary" @click="nextStep">下一步行动</button>
|
<view class="details-toggle" @click="metadataOpen = !metadataOpen"><text class="metadata-toggle">报告信息</text><text class="details-action">{{ metadataOpen ? '收起' : '展开' }}</text></view>
|
||||||
<button class="secondary" @click="consult">对报告有疑问,咨询AI</button>
|
<view v-if="metadataOpen" class="metadata"><text class="report-note">评估 #{{ assessmentId }} · 第 {{ report!.revisionNo }} 版 · {{ report!.generatedAt }}</text><text class="report-note">分析来源:用户自填资料 · {{ report!.modelCode }}</text><text class="report-note">报告已保存至融资档案。导出不含身份信息,请妥善保管下载文件。</text><text class="disclaimer">{{ report!.disclaimer }}</text><view class="report-links"><text class="report-link" @click="start">发起新评估</text><text class="report-link" @click="history">历史评估</text></view></view>
|
||||||
<view class="bottom-actions"><button class="secondary" @click="start">重新评估</button><button class="secondary" @click="history">查看历史评估</button></view>
|
<text v-show="!metadataOpen" class="disclaimer">自填资料未经核验,结果仅供参考,不代表授信或获批。</text>
|
||||||
</template>
|
</template>
|
||||||
</view>
|
</view>
|
||||||
</scroll-view>
|
</scroll-view>
|
||||||
|
<view v-if="report != null" class="report-footer" :style="bottomStyle(24)"><button class="reevaluate-button" @click="editAssessment">重新评估</button></view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="uts">
|
<script setup lang="uts">
|
||||||
import AppHeader from '@/components/AppHeader.uvue'
|
import AppIcon from '@/components/AppIcon.uvue'
|
||||||
import ReportExport from '@/packages/finance/components/ReportExport.uvue'
|
import ReportExport from '@/packages/finance/components/ReportExport.uvue'
|
||||||
import { usePageLayout } from '@/services/layout.uts'
|
import { usePageLayout } from '@/services/layout.uts'
|
||||||
import { getSession } from '@/services/api.uts'
|
import { getSession } from '@/services/api.uts'
|
||||||
@@ -71,19 +77,40 @@
|
|||||||
import { MatchPage, MatchView, MatchReason, fetchMatchPage, validRecordId } from '@/packages/finance/services/matches.uts'
|
import { MatchPage, MatchView, MatchReason, fetchMatchPage, validRecordId } from '@/packages/finance/services/matches.uts'
|
||||||
type ReportTab = { id: string, label: string }
|
type ReportTab = { id: string, label: string }
|
||||||
type AdviceGroup = { id: string, label: string, loading: boolean, error: string, items: MatchView[] }
|
type AdviceGroup = { id: string, label: string, loading: boolean, error: string, items: MatchView[] }
|
||||||
const { contentStyle, pageStyle } = usePageLayout()
|
const { contentStyle, pageStyle, statusStyle, bottomStyle, navigationTitleStyle, navigationActionStyle } = usePageLayout()
|
||||||
|
const reportTitleStyle = (): string => {
|
||||||
|
// 小程序标题须同时避让PDF按钮和右侧胶囊,窄屏也不能覆盖操作入口。
|
||||||
|
// #ifdef MP-WEIXIN
|
||||||
|
const windowWidth = uni.getWindowInfo().windowWidth
|
||||||
|
const capsule = uni.getMenuButtonBoundingClientRect()
|
||||||
|
const reserved = capsule.left > 0 && capsule.left < windowWidth ? windowWidth - capsule.left + 8 : 104
|
||||||
|
return 'left:50px;right:' + (reserved + 40) + 'px;'
|
||||||
|
// #endif
|
||||||
|
// #ifndef MP-WEIXIN
|
||||||
|
return navigationTitleStyle()
|
||||||
|
// #endif
|
||||||
|
}
|
||||||
const tabs: ReportTab[] = [{ id: 'overview', label: '评估概况' }, { id: 'matches', label: '匹配结果' }, { id: 'improvements', label: '改善建议' }]
|
const tabs: ReportTab[] = [{ id: 'overview', label: '评估概况' }, { id: 'matches', label: '匹配结果' }, { id: 'improvements', label: '改善建议' }]
|
||||||
const assessmentId = ref(''); const tab = ref('overview'); const loading = ref(false); const error = ref(''); const routeError = ref('')
|
const revisionId = ref(''); const assessmentId = ref(''); const tab = ref('overview'); const loading = ref(false); const error = ref(''); const routeError = ref('')
|
||||||
const report = ref<AnalysisReport | null>(null); const factsOpen = ref(false); const completenessOpen = ref(false)
|
const report = ref<AnalysisReport | null>(null); const factsOpen = ref(false); const completenessOpen = ref(false)
|
||||||
// 日期与导出说明按需展开,首屏优先呈现准备状态和当前栏目的内容。
|
// 日期与导出说明按需展开,首屏优先呈现准备状态和当前栏目的内容。
|
||||||
const metadataOpen = ref(false)
|
const metadataOpen = ref(false)
|
||||||
|
const detailsOpen = ref(false); const downloadStatus = ref('')
|
||||||
|
const exportStatus = (message: string) => { downloadStatus.value = message }
|
||||||
const candidates = ref<MatchView[]>([]); const candidateLoading = ref(false); const candidateError = ref('')
|
const candidates = ref<MatchView[]>([]); const candidateLoading = ref(false); const candidateError = ref('')
|
||||||
const adviceGroups = ref<AdviceGroup[]>([])
|
const adviceGroups = ref<AdviceGroup[]>([])
|
||||||
let generation = 0; let visible = false
|
let generation = 0; let visible = false
|
||||||
const login = () => { uni.redirectTo({ url: '/packages/account/login/login?resumeReport=1&assessmentId=' + assessmentId.value + '&tab=' + tab.value }) }
|
const login = () => { uni.redirectTo({ url: '/packages/account/login/login?resumeReport=1&assessmentId=' + assessmentId.value + (revisionId.value.length > 0 ? '&revisionId=' + revisionId.value : '') + '&tab=' + tab.value }) }
|
||||||
const missingItems = computed((): AssessmentField[] => report.value == null ? [] : report.value!.completenessItems.filter((item: AssessmentField): boolean => ['待补充','需核实'].includes(item.value)))
|
const missingItems = computed((): AssessmentField[] => report.value == null ? [] : report.value!.completenessItems.filter((item: AssessmentField): boolean => ['待补充','需核实'].includes(item.value)))
|
||||||
|
const priorityItems = computed((): string[] => {
|
||||||
|
const value = report.value
|
||||||
|
if (value == null) return []
|
||||||
|
// 使用已保存建议及真实缺失项,不补造原型中的固定三条或融资评分。
|
||||||
|
const items = value.suggestionSummaries.concat(missingItems.value.map((item: AssessmentField): string => (item.value == '待补充' ? '补充' : '核实') + item.label))
|
||||||
|
return items.filter((item: string, index: number): boolean => item.trim().length > 0 && items.indexOf(item) == index)
|
||||||
|
})
|
||||||
const adviceReasons = (item: MatchView): MatchReason[] => item.state == 'ineligible' ? item.blocking.concat(item.cautions) : item.cautions
|
const adviceReasons = (item: MatchView): MatchReason[] => item.state == 'ineligible' ? item.blocking.concat(item.cautions) : item.cautions
|
||||||
const reset = () => { report.value = null; candidates.value = []; adviceGroups.value = []; candidateLoading.value = false; candidateError.value = ''; factsOpen.value = false; completenessOpen.value = false; metadataOpen.value = false }
|
const reset = () => { report.value = null; candidates.value = []; adviceGroups.value = []; candidateLoading.value = false; candidateError.value = ''; factsOpen.value = false; completenessOpen.value = false; metadataOpen.value = false; detailsOpen.value = false; downloadStatus.value = '' }
|
||||||
const current = (request: number, token: string): boolean => visible && request == generation && getSession()?.getString('token') == token
|
const current = (request: number, token: string): boolean => visible && request == generation && getSession()?.getString('token') == token
|
||||||
const sessionChanged = (request: number, token: string): boolean => {
|
const sessionChanged = (request: number, token: string): boolean => {
|
||||||
if (!visible || request != generation) return true
|
if (!visible || request != generation) return true
|
||||||
@@ -95,14 +122,14 @@
|
|||||||
}
|
}
|
||||||
const verifyMatches = (page: MatchPage) => {
|
const verifyMatches = (page: MatchPage) => {
|
||||||
const value = report.value
|
const value = report.value
|
||||||
if (value == null || page.assessmentId != assessmentId.value || page.resultVersion != value.version || page.status != 'completed' || page.snapshotId != value.matching.snapshotId || page.ruleVersion != value.matching.ruleVersion || page.orderingVersion != value.matching.orderingVersion || page.eligible != value.matching.eligible || page.ineligible != value.matching.ineligible || page.unknown != value.matching.unknown) throw new Error('产品结果与本报告的冻结版本不一致,请重新加载报告。')
|
if (value == null || page.assessmentId != assessmentId.value || page.revisionId != value.revisionId || page.resultVersion != value.version || page.status != 'completed' || page.snapshotId != value.matching.snapshotId || page.ruleVersion != value.matching.ruleVersion || page.orderingVersion != value.matching.orderingVersion || page.eligible != value.matching.eligible || page.ineligible != value.matching.ineligible || page.unknown != value.matching.unknown) throw new Error('产品结果与本报告的冻结版本不一致,请重新加载报告。')
|
||||||
}
|
}
|
||||||
const loadCandidates = async () => {
|
const loadCandidates = async () => {
|
||||||
if (candidateLoading.value || report.value?.matching.status != 'completed') return
|
if (candidateLoading.value || report.value?.matching.status != 'completed') return
|
||||||
const request = generation; const token = getSession()?.getString('token') ?? ''
|
const request = generation; const token = getSession()?.getString('token') ?? ''
|
||||||
candidateLoading.value = true; candidateError.value = ''; candidates.value = []
|
candidateLoading.value = true; candidateError.value = ''; candidates.value = []
|
||||||
try {
|
try {
|
||||||
const result = await fetchMatchPage(assessmentId.value, 'eligible', '')
|
const result = await fetchMatchPage(assessmentId.value, 'eligible', '', revisionId.value)
|
||||||
if (!current(request, token)) { sessionChanged(request, token); return }
|
if (!current(request, token)) { sessionChanged(request, token); return }
|
||||||
verifyMatches(result); candidates.value = result.items
|
verifyMatches(result); candidates.value = result.items
|
||||||
} catch (err) { if (!sessionChanged(request, token)) candidateError.value = err instanceof Error ? err.message : '候选读取失败' }
|
} catch (err) { if (!sessionChanged(request, token)) candidateError.value = err instanceof Error ? err.message : '候选读取失败' }
|
||||||
@@ -114,7 +141,7 @@
|
|||||||
const request = generation; const token = getSession()?.getString('token') ?? ''
|
const request = generation; const token = getSession()?.getString('token') ?? ''
|
||||||
group.loading = true; group.error = ''; group.items = []
|
group.loading = true; group.error = ''; group.items = []
|
||||||
try {
|
try {
|
||||||
const result = await fetchMatchPage(assessmentId.value, id, '')
|
const result = await fetchMatchPage(assessmentId.value, id, '', revisionId.value)
|
||||||
if (!current(request, token)) { sessionChanged(request, token); return }
|
if (!current(request, token)) { sessionChanged(request, token); return }
|
||||||
verifyMatches(result); group.items = result.items
|
verifyMatches(result); group.items = result.items
|
||||||
} catch (err) { if (!sessionChanged(request, token)) group.error = err instanceof Error ? err.message : '产品依据读取失败' }
|
} catch (err) { if (!sessionChanged(request, token)) group.error = err instanceof Error ? err.message : '产品依据读取失败' }
|
||||||
@@ -127,9 +154,9 @@
|
|||||||
if (session == null) { login(); return }
|
if (session == null) { login(); return }
|
||||||
const token = session.getString('token') ?? ''; loading.value = true
|
const token = session.getString('token') ?? ''; loading.value = true
|
||||||
try {
|
try {
|
||||||
const result = await fetchAnalysisReport(assessmentId.value)
|
const result = await fetchAnalysisReport(assessmentId.value, revisionId.value)
|
||||||
if (!current(request, token)) { sessionChanged(request, token); return }
|
if (!current(request, token)) { sessionChanged(request, token); return }
|
||||||
report.value = result
|
report.value = result; revisionId.value = result.revisionId
|
||||||
if (tab.value == 'matches') loadCandidates()
|
if (tab.value == 'matches') loadCandidates()
|
||||||
if (tab.value == 'improvements') {
|
if (tab.value == 'improvements') {
|
||||||
adviceGroups.value = [{ id: 'ineligible', label: '不符条件', loading: false, error: '', items: [] }, { id: 'unknown', label: '待核实项', loading: false, error: '', items: [] }]
|
adviceGroups.value = [{ id: 'ineligible', label: '不符条件', loading: false, error: '', items: [] }, { id: 'unknown', label: '待核实项', loading: false, error: '', items: [] }]
|
||||||
@@ -139,13 +166,14 @@
|
|||||||
finally { if (request == generation) loading.value = false }
|
finally { if (request == generation) loading.value = false }
|
||||||
}
|
}
|
||||||
// 切栏替换当前路由,刷新恢复所选栏目;只读取已保存快照,不提交评估或触发模型。
|
// 切栏替换当前路由,刷新恢复所选栏目;只读取已保存快照,不提交评估或触发模型。
|
||||||
const switchTab = (value: string) => { if (value != tab.value && routeError.value.length == 0) uni.redirectTo({ url: '/packages/finance/report/report?assessmentId=' + assessmentId.value + '&tab=' + value }) }
|
const switchTab = (value: string) => { if (value != tab.value && routeError.value.length == 0) uni.redirectTo({ url: '/packages/finance/report/report?assessmentId=' + assessmentId.value + (revisionId.value.length > 0 ? '&revisionId=' + revisionId.value : '') + '&tab=' + value }) }
|
||||||
|
const editAssessment = () => { uni.navigateTo({ url: '/packages/finance/assessment/assessment?editId=' + assessmentId.value }) }
|
||||||
const history = () => { uni.navigateTo({ url: '/packages/finance/archive/archive' }) }
|
const history = () => { uni.navigateTo({ url: '/packages/finance/archive/archive' }) }
|
||||||
const consult = () => { uni.navigateTo({ url: assistantRoute(assessmentId.value, [], 'report') }) }
|
const consult = () => { uni.navigateTo({ url: assistantRoute(assessmentId.value, [], 'report', '', revisionId.value) }) }
|
||||||
const nextStep = () => { if (report.value != null) uni.navigateTo({ url: '/packages/finance/next-step/next-step?assessmentId=' + assessmentId.value + '§ion=actions' }) }
|
const nextStep = () => { if (report.value != null) uni.navigateTo({ url: '/packages/finance/next-step/next-step?assessmentId=' + assessmentId.value + (revisionId.value.length > 0 ? '&revisionId=' + revisionId.value : '') + '§ion=actions' }) }
|
||||||
const back = () => { if (getCurrentPages().length > 1) uni.navigateBack(); else uni.redirectTo({ url: '/packages/finance/archive/archive' }) }
|
const back = () => { if (getCurrentPages().length > 1) uni.navigateBack(); else uni.redirectTo({ url: '/packages/finance/archive/archive' }) }
|
||||||
const allMatches = (group: string) => { uni.navigateTo({ url: '/packages/finance/matches/matches?assessmentId=' + assessmentId.value + '&group=' + group }) }
|
const allMatches = (group: string) => { uni.navigateTo({ url: '/packages/finance/matches/matches?assessmentId=' + assessmentId.value + (revisionId.value.length > 0 ? '&revisionId=' + revisionId.value : '') + '&group=' + group }) }
|
||||||
const openProduct = (id: string) => { uni.navigateTo({ url: '/packages/finance/product/product?assessmentId=' + assessmentId.value + '&productId=' + id + '&tab=explanation&from=report' }) }
|
const openProduct = (id: string) => { uni.navigateTo({ url: '/packages/finance/product/product?assessmentId=' + assessmentId.value + (revisionId.value.length > 0 ? '&revisionId=' + revisionId.value : '') + '&productId=' + id + '&tab=explanation&from=report' }) }
|
||||||
const start = () => {
|
const start = () => {
|
||||||
const token = getSession()?.getString('token') ?? ''; const request = generation
|
const token = getSession()?.getString('token') ?? ''; const request = generation
|
||||||
uni.showActionSheet({ itemList: ['个人评估','企业评估'], success: (result) => {
|
uni.showActionSheet({ itemList: ['个人评估','企业评估'], success: (result) => {
|
||||||
@@ -155,6 +183,8 @@
|
|||||||
} })
|
} })
|
||||||
}
|
}
|
||||||
onLoad((options) => {
|
onLoad((options) => {
|
||||||
|
revisionId.value = options?.['revisionId'] ?? '';
|
||||||
|
if (revisionId.value.length > 0 && !validRecordId(revisionId.value)) routeError.value = '版本编号无效,请返回重新选择。'
|
||||||
assessmentId.value = options?.['assessmentId'] ?? ''; tab.value = options?.['tab'] ?? 'overview'
|
assessmentId.value = options?.['assessmentId'] ?? ''; tab.value = options?.['tab'] ?? 'overview'
|
||||||
if (!validRecordId(assessmentId.value)) routeError.value = '请从融资档案选择有效的评估记录。'
|
if (!validRecordId(assessmentId.value)) routeError.value = '请从融资档案选择有效的评估记录。'
|
||||||
if (!['overview','matches','improvements'].includes(tab.value)) routeError.value = '报告栏目参数无效,请从融资档案重新进入。'
|
if (!['overview','matches','improvements'].includes(tab.value)) routeError.value = '报告栏目参数无效,请从融资档案重新进入。'
|
||||||
@@ -165,28 +195,54 @@
|
|||||||
onUnload(() => { visible = false; generation++; reset() })
|
onUnload(() => { visible = false; generation++; reset() })
|
||||||
</script>
|
</script>
|
||||||
<style>
|
<style>
|
||||||
.tabs { height: 90rpx; flex-direction: row; background-color: #fff; border-bottom: 1rpx solid #e5ecf5; }
|
.page { background-color: #fff; }
|
||||||
|
.status-bar { background-color: #fff; }
|
||||||
|
.report-header { height: 96rpx; padding: 0 28rpx; flex-direction: row; align-items: center; justify-content: space-between; position: relative; background-color: #fff; }
|
||||||
|
.header-back { width: 64rpx; height: 96rpx; justify-content: center; align-items: flex-start; }
|
||||||
|
.back-icon { transform: rotate(180deg); }
|
||||||
|
.header-title-area { position: absolute; left: 140rpx; right: 140rpx; top: 0; bottom: 0; align-items: center; justify-content: center; }
|
||||||
|
.header-title { color: #17243b; font-size: 32rpx; font-weight: 700; }
|
||||||
|
.header-export { align-items: center; justify-content: center; }
|
||||||
|
/* #ifdef MP-WEIXIN */
|
||||||
|
.header-title { font-size: 27rpx; }
|
||||||
|
/* #endif */
|
||||||
|
.tabs { height: 82rpx; flex-direction: row; background-color: #fff; padding: 0 28rpx; border-bottom: 1rpx solid #eff2f6; }
|
||||||
.tab { flex: 1; align-items: center; justify-content: center; }
|
.tab { flex: 1; align-items: center; justify-content: center; }
|
||||||
.active { border-bottom: 4rpx solid #0868f7; }
|
.active { border-bottom: 4rpx solid #0868f7; }
|
||||||
.tab-label { font-size: 27rpx; color: #7b8ba0; }
|
.tab-label { font-size: 26rpx; color: #737e90; }
|
||||||
.selected { color: #0868f7; font-weight: 700; }
|
.selected { color: #0868f7; font-weight: 700; }
|
||||||
.report-content { padding: 28rpx 28rpx 65rpx; }
|
.revision-notice { font-size: 24rpx; line-height: 38rpx; color: #886934; padding: 18rpx 22rpx; background-color: #fff8eb; border-radius: 12rpx; margin-bottom: 12rpx; }
|
||||||
|
.download-status { font-size: 23rpx; line-height: 36rpx; color: #547292; padding-bottom: 18rpx; }
|
||||||
|
.report-content { padding: 28rpx 30rpx 24rpx; }
|
||||||
.eyebrow { color: #778ba3; font-size: 22rpx; }
|
.eyebrow { color: #778ba3; font-size: 22rpx; }
|
||||||
.headline { color: #1a3859; font-size: 34rpx; font-weight: 700; margin-top: 22rpx; }
|
.headline { color: #1a3859; font-size: 34rpx; font-weight: 700; margin-top: 22rpx; }
|
||||||
.intro { color: #73869d; font-size: 24rpx; line-height: 40rpx; margin-top: 14rpx; }
|
.intro { color: #73869d; font-size: 24rpx; line-height: 40rpx; margin-top: 14rpx; }
|
||||||
.metadata-toggle { color: #78889e; font-size: 21rpx; line-height: 36rpx; padding-top: 8rpx; padding-bottom: 4rpx; }
|
.metadata-toggle { color: #8893a2; font-size: 22rpx; line-height: 36rpx; }
|
||||||
.metadata { padding: 0 0 12rpx; }
|
.metadata { padding: 0 0 12rpx; }
|
||||||
.hero { background-color: #eef5ff; border: 1rpx solid #dfeafb; border-radius: 18rpx; padding: 26rpx; margin-top: 18rpx; }
|
.readiness-band { padding: 28rpx 30rpx; min-height: 154rpx; background-color: #eef9f3; }
|
||||||
.hero-label { color: #627c9c; font-size: 24rpx; }
|
.attention { background-color: #fff8ed; }
|
||||||
.hero-title { color: #165cb9; font-size: 42rpx; font-weight: 700; line-height: 56rpx; margin-top: 8rpx; }
|
.readiness-label { color: #526272; font-size: 25rpx; line-height: 38rpx; }
|
||||||
.hero-note { color: #7086a1; font-size: 22rpx; line-height: 34rpx; margin-top: 8rpx; }
|
.readiness-title { color: #17a46b; font-size: 46rpx; line-height: 64rpx; font-weight: 700; margin-top: 9rpx; }
|
||||||
.hero-divider { height: 1rpx; background-color: #d9e5f6; margin-top: 18rpx; margin-bottom: 16rpx; }
|
.attention .readiness-title { color: #a57925; }
|
||||||
|
.summary { font-size: 26rpx; line-height: 44rpx; color: #4d5869; margin-top: 24rpx; }
|
||||||
|
.priority-section { margin-top: 30rpx; }
|
||||||
|
.priority-row { flex-direction: row; align-items: flex-start; padding: 24rpx 0; border-bottom: 1rpx solid #edf0f4; min-height: 88rpx; }
|
||||||
|
.priority-number { width: 36rpx; height: 36rpx; border-radius: 18rpx; background-color: #f66f4e; text-align: center; color: #fff; font-size: 22rpx; line-height: 36rpx; margin-right: 18rpx; margin-top: 2rpx; flex-shrink: 0; }
|
||||||
|
.priority-text { flex: 1; font-size: 26rpx; line-height: 40rpx; color: #485568; }
|
||||||
|
.more-advice { font-size: 23rpx; line-height: 38rpx; color: #0868f7; margin-top: 18rpx; }
|
||||||
|
.details-toggle { flex-direction: row; align-items: center; justify-content: space-between; padding: 22rpx 0; border-bottom: 1rpx solid #edf0f4; }
|
||||||
|
.details-label { font-size: 24rpx; color: #738094; }
|
||||||
|
.details-action { font-size: 23rpx; color: #8794a5; }
|
||||||
|
.report-links { flex-direction: row; align-items: center; justify-content: space-between; margin-top: 22rpx; }
|
||||||
|
.report-link { font-size: 24rpx; line-height: 38rpx; color: #4e77aa; padding: 8rpx 0; }
|
||||||
|
.report-footer { position: fixed; left: 0; right: 0; bottom: 0; padding: 20rpx 30rpx 24rpx; background-color: #fff; }
|
||||||
|
.reevaluate-button { width: 100%; height: 92rpx; line-height: 92rpx; font-size: 28rpx; font-weight: 600; border-radius: 14rpx; background-color: #0868f7; color: #fff; margin: 0; }
|
||||||
.percent { color: #285c9d; font-size: 30rpx; font-weight: 700; }
|
.percent { color: #285c9d; font-size: 30rpx; font-weight: 700; }
|
||||||
.progress-track { height: 8rpx; border-radius: 4rpx; background-color: #dbe7f8; margin-top: 12rpx; overflow: hidden; }
|
.progress-track { height: 8rpx; border-radius: 4rpx; background-color: #dbe7f8; margin-top: 12rpx; overflow: hidden; }
|
||||||
.progress-fill { height: 8rpx; background-color: #4c8beb; border-radius: 4rpx; }
|
.progress-fill { height: 8rpx; background-color: #4c8beb; border-radius: 4rpx; }
|
||||||
.panel { margin-top: 24rpx; padding: 28rpx; border-radius: 18rpx; background-color: #fff; border: 1rpx solid #e7edf6; }
|
.panel { margin-top: 28rpx; padding: 0 0 24rpx; border-bottom: 1rpx solid #edf0f4; }
|
||||||
.panel-heading { flex-direction: row; justify-content: space-between; align-items: center; }
|
.panel-heading { flex-direction: row; justify-content: space-between; align-items: center; }
|
||||||
.section-title { color: #253f5e; font-size: 29rpx; font-weight: 700; line-height: 42rpx; }
|
.section-title { color: #233750; font-size: 28rpx; font-weight: 700; line-height: 42rpx; }
|
||||||
.toggle { font-size: 24rpx; color: #0868f7; padding: 10rpx; }
|
.toggle { font-size: 24rpx; color: #0868f7; padding: 10rpx; }
|
||||||
.body-text { margin-top: 20rpx; font-size: 26rpx; color: #425b78; line-height: 44rpx; }
|
.body-text { margin-top: 20rpx; font-size: 26rpx; color: #425b78; line-height: 44rpx; }
|
||||||
.report-note { font-size: 23rpx; line-height: 38rpx; color: #78889e; margin-top: 12rpx; }
|
.report-note { font-size: 23rpx; line-height: 38rpx; color: #78889e; margin-top: 12rpx; }
|
||||||
@@ -195,11 +251,11 @@
|
|||||||
.fact-label { flex: 1; font-size: 25rpx; line-height: 39rpx; color: #738198; }
|
.fact-label { flex: 1; font-size: 25rpx; line-height: 39rpx; color: #738198; }
|
||||||
.fact-value { flex: 1; text-align: right; font-size: 25rpx; line-height: 39rpx; color: #233e5e; }
|
.fact-value { flex: 1; text-align: right; font-size: 25rpx; line-height: 39rpx; color: #233e5e; }
|
||||||
.legacy { background-color: #fff5e7; padding: 16rpx 22rpx 24rpx; border-radius: 14rpx; margin-top: 20rpx; }
|
.legacy { background-color: #fff5e7; padding: 16rpx 22rpx 24rpx; border-radius: 14rpx; margin-top: 20rpx; }
|
||||||
.stats { flex-direction: row; background-color: #e9f2ff; padding: 26rpx 8rpx; border-radius: 18rpx; margin-top: 26rpx; }
|
.stats { flex-direction: row; padding: 26rpx 8rpx; border-top: 1rpx solid #edf0f4; border-bottom: 1rpx solid #edf0f4; margin-top: 26rpx; }
|
||||||
.stat { flex: 1; align-items: center; }
|
.stat { flex: 1; align-items: center; }
|
||||||
.stat-number { font-size: 39rpx; color: #1b5ebd; font-weight: 700; }
|
.stat-number { font-size: 39rpx; color: #1b5ebd; font-weight: 700; }
|
||||||
.stat-label { font-size: 22rpx; color: #5c7799; margin-top: 12rpx; }
|
.stat-label { font-size: 22rpx; color: #5c7799; margin-top: 12rpx; }
|
||||||
.product-card { padding: 28rpx; border-radius: 18rpx; background-color: #fff; border: 1rpx solid #e0eaf7; margin-top: 24rpx; }
|
.product-card { padding: 28rpx; border-radius: 14rpx; background-color: #fff; border: 1rpx solid #e0eaf7; margin-top: 24rpx; }
|
||||||
.product-name { flex: 1; color: #284566; font-size: 28rpx; line-height: 42rpx; font-weight: 700; }
|
.product-name { flex: 1; color: #284566; font-size: 28rpx; line-height: 42rpx; font-weight: 700; }
|
||||||
.version { color: #8b9aab; font-size: 22rpx; margin-left: 14rpx; }
|
.version { color: #8b9aab; font-size: 22rpx; margin-left: 14rpx; }
|
||||||
.demo { color: #9b762d; font-size: 22rpx; margin-top: 12rpx; }
|
.demo { color: #9b762d; font-size: 22rpx; margin-top: 12rpx; }
|
||||||
@@ -214,7 +270,12 @@
|
|||||||
.evidence { color: #8494a8; font-size: 22rpx; line-height: 36rpx; margin-top: 12rpx; }
|
.evidence { color: #8494a8; font-size: 22rpx; line-height: 36rpx; margin-top: 12rpx; }
|
||||||
.primary { background-color: #0868f7; color: #fff; font-size: 27rpx; border-radius: 12rpx; margin-top: 24rpx; }
|
.primary { background-color: #0868f7; color: #fff; font-size: 27rpx; border-radius: 12rpx; margin-top: 24rpx; }
|
||||||
.secondary { background-color: #eaf3ff; color: #0868f7; font-size: 25rpx; border-radius: 12rpx; margin-top: 22rpx; }
|
.secondary { background-color: #eaf3ff; color: #0868f7; font-size: 25rpx; border-radius: 12rpx; margin-top: 22rpx; }
|
||||||
.disclaimer { color: #8897aa; font-size: 22rpx; line-height: 36rpx; margin-top: 28rpx; }
|
.disclaimer { color: #939caa; font-size: 21rpx; line-height: 34rpx; margin-top: 18rpx; }
|
||||||
.bottom-actions { flex-direction: row; justify-content: space-between; }
|
/* #ifndef APP */
|
||||||
.bottom-actions .secondary { width: 48%; }
|
@media (min-width: 768px) {
|
||||||
|
.report-content { max-width: 720px; margin: 0 auto; }
|
||||||
|
.report-footer { align-items: center; }
|
||||||
|
.reevaluate-button { max-width: 690px; }
|
||||||
|
}
|
||||||
|
/* #endif */
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
import { revisionParams } from './records.uts'
|
||||||
import { requestJson } from '@/services/api.uts'
|
import { requestJson } from '@/services/api.uts'
|
||||||
export type AssistantProduct = { id: string, name: string, state: string, stateLabel: string, demo: boolean }
|
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 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 AssistantTurn = { id: string, requestId: string, message: string, reply: string, status: string, attempt: number, canRetry: boolean }
|
||||||
export type AssistantPage = { items: AssistantTurn[], more: boolean, cursor: string }
|
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 }
|
export type AssistantHistoryPage = { items: AssistantHistory[], more: boolean, cursor: string }
|
||||||
function text(data: UTSJSONObject, key: string): string { return data.getString(key) ?? '' }
|
function text(data: UTSJSONObject, key: string): string { return data.getString(key) ?? '' }
|
||||||
function contextView(data: UTSJSONObject): AssistantContext {
|
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 })),
|
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') }
|
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 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 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 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 {
|
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 : '')
|
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 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): Promise<AssistantChat> {
|
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) : new UTSJSONObject()))
|
const result = chatView(await requestJson('/assistant/chats/' + chatId, 'GET', checkBinding ? contextQuery(assessmentId, ids, revisionId) : new UTSJSONObject()))
|
||||||
if (result.id != chatId) throw new Error('返回的会话不一致,请重新加载。')
|
if (result.id != chatId) throw new Error('返回的会话不一致,请重新加载。')
|
||||||
return result
|
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> {
|
export async function assistantMessages(chatId: string, cursor: string): Promise<AssistantPage> {
|
||||||
const data = await requestJson('/assistant/chats/' + chatId + '/messages', 'GET', { limit: '20', cursor })
|
const data = await requestJson('/assistant/chats/' + chatId + '/messages', 'GET', { limit: '20', cursor })
|
||||||
if (text(data, 'chatId') != chatId) throw new Error('消息不属于本次会话。')
|
if (text(data, 'chatId') != chatId) throw new Error('消息不属于本次会话。')
|
||||||
|
|||||||
@@ -17,21 +17,21 @@ export function parseCompareIds(raw: string): string[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 只缓存少量选择ID,不持久化产品或个人事实;换账号、会话或评估后立即隔离。
|
// 只缓存少量选择ID,不持久化产品或个人事实;换账号、会话或评估后立即隔离。
|
||||||
export function compareSelection(assessmentId: string): string[] {
|
export function compareSelection(assessmentId: string, revisionId: string = ''): string[] {
|
||||||
const session = getSession()
|
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 = [] }
|
if (key != scope || key.length == 0) { scope = key; selected = [] }
|
||||||
return selected.slice()
|
return selected.slice()
|
||||||
}
|
}
|
||||||
export function saveCompareSelection(assessmentId: string, ids: string[]): void {
|
export function saveCompareSelection(assessmentId: string, ids: string[], revisionId: string = ''): void {
|
||||||
compareSelection(assessmentId)
|
compareSelection(assessmentId, revisionId)
|
||||||
selected = parseCompareIds(ids.join(','))
|
selected = parseCompareIds(ids.join(','))
|
||||||
}
|
}
|
||||||
export function compareQuery(assessmentId: string, ids: string[]): string {
|
export function compareQuery(assessmentId: string, ids: string[], revisionId: string = ''): string {
|
||||||
return '?assessmentId=' + encodeURIComponent(assessmentId) + '&productIds=' + ids.join(',')
|
return '?assessmentId=' + encodeURIComponent(assessmentId) + '&productIds=' + ids.join(',') + (revisionId.length > 0 ? '&revisionId=' + revisionId : '')
|
||||||
}
|
}
|
||||||
export function sameMatchSnapshot(a: MatchDetail, b: MatchDetail): boolean {
|
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 {
|
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')
|
return reasons.length == 0 ? empty : reasons.map((r: MatchReason): string => r.text + '\n' + r.evidence.join('\n')).join('\n\n')
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
|
import { revisionParams } from './records.uts'
|
||||||
import { requestJson } from '@/services/api.uts'
|
import { requestJson } from '@/services/api.uts'
|
||||||
import { yuanToWan } from '@/services/money.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 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 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 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, aiGeneratedAt: string }
|
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, 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 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) ?? '' }
|
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) }
|
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() + ' 个月',
|
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('、'),
|
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') : '未提供',
|
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 {
|
function matchView(data: UTSJSONObject): MatchView {
|
||||||
const evidence = data['evidence'] as UTSJSONObject[]
|
const evidence = data['evidence'] as UTSJSONObject[]
|
||||||
@@ -72,35 +74,35 @@ function matchView(data: UTSJSONObject): MatchView {
|
|||||||
availabilityLabel: availability == 'available' ? '当前产品可查看' : (availability == 'unavailable' ? '当前产品已下架或不在有效期' : '当前产品状态暂无法确认'),
|
availabilityLabel: availability == 'available' ? '当前产品可查看' : (availability == 'unavailable' ? '当前产品已下架或不在有效期' : '当前产品状态暂无法确认'),
|
||||||
product: productView(data['productSnapshot'] as UTSJSONObject, false), positive: reasons('positiveReasons'), blocking: reasons('blockingReasons'), cautions: reasons('cautions'),
|
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解释。'),
|
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> {
|
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', { requestId })
|
const data = await requestJson('/assessments/' + encodeURIComponent(id) + '/matches/' + encodeURIComponent(productId) + '/explanation', 'POST', { ...revisionParams(revisionId), requestId })
|
||||||
const item = matchView(data['item'] as UTSJSONObject)
|
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
|
return item
|
||||||
}
|
}
|
||||||
export async function fetchMatchPage(id: string, group: string, cursor: string): Promise<MatchPage> {
|
export async function fetchMatchPage(id: string, group: string, cursor: string, revisionId: string = ''): Promise<MatchPage> {
|
||||||
const data = await requestJson('/assessments/' + encodeURIComponent(id) + '/matches', 'GET', { eligibility: group, limit: '3', cursor })
|
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')
|
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 reason = text(matching, 'reasonCode')
|
||||||
const count = (key: string): string => { const v = matching.getNumber(key); return v == null ? '—' : v.toString() }
|
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' ? '本次评估尚未启用产品匹配规则。' : '本次评估没有可用产品目录,未执行产品匹配。'))
|
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> {
|
export async function fetchMatchDetail(id: string, productId: string, revisionId: string = ''): Promise<MatchView> {
|
||||||
return (await fetchMatchDetailRecord(id, productId)).item
|
return (await fetchMatchDetailRecord(id, productId, revisionId)).item
|
||||||
}
|
}
|
||||||
export async function fetchMatchDetailRecord(id: string, productId: string): Promise<MatchDetail> {
|
export async function fetchMatchDetailRecord(id: string, productId: string, revisionId: string = ''): Promise<MatchDetail> {
|
||||||
const data = await requestJson('/assessments/' + encodeURIComponent(id) + '/matches/' + encodeURIComponent(productId), 'GET', new UTSJSONObject())
|
const data = await requestJson('/assessments/' + encodeURIComponent(id) + '/matches/' + encodeURIComponent(productId), 'GET', revisionParams(revisionId))
|
||||||
const matching = data['matching'] as UTSJSONObject
|
const matching = data['matching'] as UTSJSONObject
|
||||||
const item = matchView(data['item'] as UTSJSONObject)
|
const item = matchView(data['item'] as UTSJSONObject)
|
||||||
const version = data.getNumber('resultVersion') ?? 0
|
const version = data.getNumber('resultVersion') ?? 0
|
||||||
const snapshotId = text(matching, 'catalogSnapshotId')
|
const snapshotId = text(matching, 'catalogSnapshotId')
|
||||||
// URL只表达选择意图;读取后再次检查归属和冻结版本,不能用公共资料补齐失败列。
|
// 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('产品不属于本次冻结匹配结果,请返回重新选择。')
|
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, resultVersion: version, snapshotId, ruleVersion: text(matching, 'ruleVersion'), orderingVersion: text(matching, 'orderingPolicyVersion'), item }
|
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> {
|
export async function fetchPublicProduct(id: string): Promise<ProductView> {
|
||||||
return productView(await requestJson('/products/' + encodeURIComponent(id), 'GET', new UTSJSONObject()), true)
|
return productView(await requestJson('/products/' + encodeURIComponent(id), 'GET', new UTSJSONObject()), true)
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { getSession, requestJson } from '@/services/api.uts'
|
import { getSession, requestJson } from '@/services/api.uts'
|
||||||
import { assessmentApiPayload, draftText } from '@/services/assessment.uts'
|
import { assessmentApiPayload, draftText } from '@/services/assessment.uts'
|
||||||
import { assessmentRequestKey } from '@/services/assessment-flow.uts'
|
import { assessmentRequestKey } from '@/services/assessment-flow.uts'
|
||||||
import { yuanToWan, wanToYuan } from '@/services/money.uts'
|
import { yuanToWan } 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 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 AssessmentPage = { items: AssessmentRecord[], hasMore: boolean, nextCursor: string }
|
||||||
export type AssessmentField = { label: string, value: string }
|
export type AssessmentField = { label: string, value: string }
|
||||||
export type ArchiveSection = { id: string, icon: string, title: string, note: string, fields: AssessmentField[], provided: number }
|
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 }
|
export type ArchiveCompleteness = { percent: number | null, description: string }
|
||||||
|
|
||||||
// 档案只消费报告中已保存的完整度,不用当前表单规则重算旧评估,也不把缺失当作0%。
|
// 档案只消费报告中已保存的完整度,不用当前表单规则重算旧评估,也不把缺失当作0%。
|
||||||
export async function fetchArchiveCompleteness(id: string): Promise<ArchiveCompleteness> {
|
export async function fetchArchiveCompleteness(id: string, revisionId: string = ''): Promise<ArchiveCompleteness> {
|
||||||
const report = await requestJson('/assessments/' + encodeURIComponent(id) + '/report', 'GET', {})
|
const report = await requestJson('/assessments/' + encodeURIComponent(id) + '/report', 'GET', revisionParams(revisionId))
|
||||||
if (report.getString('assessmentId') != id) throw new Error('报告与当前档案不一致')
|
if (report.getString('assessmentId') != id) throw new Error('报告与当前档案不一致')
|
||||||
if (report.getNumber('resultVersion') == 1) return { percent: null, description: '这份旧版报告未计算完整度,已提交资料仍可查看。' }
|
if (report.getNumber('resultVersion') == 1) return { percent: null, description: '这份旧版报告未计算完整度,已提交资料仍可查看。' }
|
||||||
if (report.getNumber('resultVersion') != 2) throw new Error('报告格式暂不支持')
|
if (report.getNumber('resultVersion') != 2) throw new Error('报告格式暂不支持')
|
||||||
@@ -29,7 +28,7 @@ function recordFrom(item: UTSJSONObject): AssessmentRecord {
|
|||||||
const date = new Date(submitted)
|
const date = new Date(submitted)
|
||||||
const pad = (value: number): string => value < 10 ? '0' + value.toString() : value.toString()
|
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())
|
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> {
|
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 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 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): Promise<AnalysisReport> {
|
export async function fetchAnalysisReport(id: string, revisionId: string = ''): Promise<AnalysisReport> {
|
||||||
const result = await requestJson('/assessments/' + encodeURIComponent(id) + '/report', 'GET', {})
|
const result = await requestJson('/assessments/' + encodeURIComponent(id) + '/report', 'GET', revisionParams(revisionId))
|
||||||
const version = result['resultVersion'] as number
|
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('报告格式不支持')
|
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 date = new Date(draftText(result, 'generatedAt'))
|
||||||
const pad = (value: number): string => value < 10 ? '0' + value.toString() : value.toString()
|
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 basis = readiness['basis'] as UTSJSONObject[]
|
||||||
const stateLabels: UTSJSONObject = { provided: '已填写', missing: '待补充', invalid: '需核实', not_applicable: '不适用' }
|
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 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: yuanToWan(draftText(demandData, 'requestedAmountYuan')) + ' 万元' },
|
||||||
{ label: '融资期限', value: (demandData['requestedTermMonths'] as number).toString() + ' 个月' },
|
{ label: '融资期限', value: (demandData['requestedTermMonths'] as number).toString() + ' 个月' },
|
||||||
{ label: '资金用途', value: draftText(demandData, 'purposeLabel') }
|
{ label: '资金用途', value: draftText(demandData, 'purposeLabel') }
|
||||||
@@ -100,7 +100,7 @@ export async function fetchAnalysisReport(id: string): Promise<AnalysisReport> {
|
|||||||
}).filter((label: string): boolean => label.length > 0)
|
}).filter((label: string): boolean => label.length > 0)
|
||||||
return draftText(item, 'text') + (labels.length > 0 ? '(依据:' + labels.join(';') + ')' : '')
|
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'),
|
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') : '未评定',
|
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'])
|
const fact = rawFacts.find((entry: UTSJSONObject): boolean => entry['id'] == item['factId'])
|
||||||
return (fact == null ? '' : draftText(fact, 'label') + ':') + draftText(item, 'text')
|
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 {
|
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> {
|
export async function createAssessment(payload: UTSJSONObject): Promise<string> {
|
||||||
const session = getSession()
|
const session = getSession()
|
||||||
if (session == null) throw new Error('请先登录')
|
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 documents = payload['documents'] as UTSJSONObject[] | null
|
||||||
const grants = payload['authorizations'] as string[] | null
|
const grants = payload['authorizations'] as string[] | null
|
||||||
if ((documents != null && documents.length > 0) || (grants != null && grants.length > 0)) throw new Error('资料上传与外部查询授权尚未开放,请移除资料及授权选择后提交')
|
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 cityCodes = ['440300', '440100', '110100', '310100', '330100']
|
||||||
const cityIndex = cityNames.indexOf(draftText(values, 'city'))
|
const cityIndex = cityNames.indexOf(draftText(values, 'city'))
|
||||||
form['cityCode'] = cityIndex < 0 ? null : cityCodes[cityIndex]
|
form['cityCode'] = cityIndex < 0 ? null : cityCodes[cityIndex]
|
||||||
// 需求金额单独命名单位,仅在提交边界换算,避免与收入及负债混用。
|
// 输入v3恢复原资料表单,不提交已取消的资金需求字段;报告仍使用v2展示结构。
|
||||||
form['requestedAmountYuan'] = wanToYuan(draftText(payload, 'requestedAmountWan'))
|
const body: UTSJSONObject = { inputVersion: 3, type: values['type'], moneyUnit: 'yuan', form, documents: [] as UTSJSONObject[], consentIds: [] as string[] }
|
||||||
form['requestedTermMonths'] = parseInt(draftText(payload, 'requestedTermMonths'))
|
const editId = draftText(payload, 'editAssessmentId')
|
||||||
form['purposeCode'] = draftText(payload, 'purposeCode')
|
if (editId.length > 0) body['baseRevisionId'] = draftText(payload, 'baseRevisionId')
|
||||||
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, editId + ':' + JSON.stringify(body))
|
||||||
body['requestId'] = assessmentRequestKey(session['token'] as string, JSON.stringify(body))
|
const path = editId.length > 0 ? '/assessments/' + encodeURIComponent(editId) + '/revisions' : '/assessments'
|
||||||
const result = await requestJson('/assessments', 'POST', body)
|
const result = await requestJson(path, 'POST', body)
|
||||||
const id = draftText(result, 'assessmentId')
|
const id = draftText(result, 'assessmentId')
|
||||||
if (id.length == 0) throw new Error('提交回执不完整,请重试')
|
if (id.length == 0) throw new Error('提交回执不完整,请重试')
|
||||||
return id
|
return id
|
||||||
@@ -176,14 +174,14 @@ export async function fetchAssessmentStatus(id: string): Promise<AssessmentRecor
|
|||||||
return recordFrom(await requestJson('/assessments/' + encodeURIComponent(id) + '/status', 'GET', {}))
|
return recordFrom(await requestJson('/assessments/' + encodeURIComponent(id) + '/status', 'GET', {}))
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchAssessmentDetail(id: string): Promise<AssessmentDetail> {
|
export async function fetchAssessmentDetail(id: string, revisionId: string = ''): Promise<AssessmentDetail> {
|
||||||
const result = await requestJson('/assessments/' + encodeURIComponent(id), 'GET', {})
|
const result = await requestJson('/assessments/' + encodeURIComponent(id), 'GET', revisionParams(revisionId))
|
||||||
const form = result['form'] as UTSJSONObject | null
|
const form = result['form'] as UTSJSONObject | null
|
||||||
if (form == null) throw new Error('评估详情不完整')
|
if (form == null) throw new Error('评估详情不完整')
|
||||||
const fields: AssessmentField[] = []
|
const fields: AssessmentField[] = []
|
||||||
const enterprise = result['type'] == 'enterprise'
|
const enterprise = result['type'] == 'enterprise'
|
||||||
const sections: ArchiveSection[] = [
|
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: 'income', icon: 'income', title: enterprise ? '经营信息' : '职业与收入', note: enterprise ? '经营行业、月数与营收' : '职业、收入与缴纳情况', fields: [], provided: 0 },
|
||||||
{ id: 'assets', icon: 'asset', title: '资产与负债', note: '贷款、信用卡额度与房车情况', fields: [], provided: 0 },
|
{ id: 'assets', icon: 'asset', title: '资产与负债', note: '贷款、信用卡额度与房车情况', fields: [], provided: 0 },
|
||||||
{ id: 'authorization', icon: 'authorization', 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++
|
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 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 labels = ['姓名 / 联系人', '手机号', '城市', '身份证号', '企业名称', '信用代码', '职业 / 行业', '工作单位', '经营月数', '月收入', '年营收', '信用贷款笔数', '欠款总余额', '贷款月还款金额', '信用卡张数', '信用卡总额度', '信用卡已用额度', '婚姻状况', '社保缴纳', '公积金缴纳', '是否有房', '是否有车']
|
||||||
const moneyKeys = ['monthlyIncome', 'annualRevenue', 'totalDebtBalance', 'monthlyLoanPayment', 'creditCardTotalLimit', 'creditCardUsedLimit']
|
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: '未缴纳' }
|
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 }
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { APP_ENVIRONMENT } from '@/services/config.uts'
|
|||||||
|
|
||||||
export type ReportDownload = { cancel: () => void, done: Promise<string> }
|
export type ReportDownload = { cancel: () => void, done: Promise<string> }
|
||||||
|
|
||||||
export function downloadReportPdf(id: string): ReportDownload {
|
export function downloadReportPdf(id: string, revisionId: string = ''): ReportDownload {
|
||||||
// 首版仅开放普通浏览器H5;其他端明确提示,不生成假文件或成功回执。
|
// 首版仅开放普通浏览器H5;其他端明确提示,不生成假文件或成功回执。
|
||||||
// #ifndef H5
|
// #ifndef H5
|
||||||
return { cancel: () => {}, done: new Promise<string>((_resolve, reject) => { reject(new Error('当前客户端暂未开放PDF下载,请在普通浏览器打开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)
|
const timeout = setTimeout(() => controller.abort(), 45000)
|
||||||
try {
|
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' },
|
method: 'GET', headers: { Authorization: 'Bearer ' + token, 'X-Requested-With': 'XMLHttpRequest' },
|
||||||
cache: 'no-store', credentials: 'omit', redirect: 'error', signal: controller.signal
|
cache: 'no-store', credentials: 'omit', redirect: 'error', signal: controller.signal
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
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 ''
|
|
||||||
}
|
|
||||||
@@ -9,14 +9,13 @@ export type AssessmentForm = {
|
|||||||
housingFund: string, house: string, car: string, savings: string, creditCard: string,
|
housingFund: string, house: string, car: string, savings: string, creditCard: string,
|
||||||
creditLoanCount: string, totalDebtBalance: string, monthlyLoanPayment: string,
|
creditLoanCount: string, totalDebtBalance: string, monthlyLoanPayment: string,
|
||||||
creditCardCount: string, creditCardTotalLimit: string, creditCardUsedLimit: string,
|
creditCardCount: string, creditCardTotalLimit: string, creditCardUsedLimit: string,
|
||||||
otherAssets: string, enterpriseName: string, businessCode: string, annualRevenue: string, operatingYears: string, operatingMonths: 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 SelectedDocument = { name: string, path: string }
|
||||||
export type DataAuthorization = { key: string, name: string, selected: boolean }
|
export type DataAuthorization = { key: string, name: string, selected: boolean }
|
||||||
|
|
||||||
export function emptyAssessmentForm(): AssessmentForm {
|
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: '', operatingMonths: '', requestedAmountWan: '', requestedTermMonths: '', purposeCode: '', purposeLabel: '', purposeDetail: '' }
|
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: '' }
|
||||||
}
|
}
|
||||||
export function draftText(saved: UTSJSONObject, key: string): string {
|
export function draftText(saved: UTSJSONObject, key: string): string {
|
||||||
const value = saved[key]
|
const value = saved[key]
|
||||||
@@ -48,8 +47,7 @@ export function formFromInput(saved: UTSJSONObject): AssessmentForm {
|
|||||||
house: ownershipFromInput(saved, 'house'), car: ownershipFromInput(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'),
|
creditLoanCount: draftText(saved, 'creditLoanCount'), totalDebtBalance: draftText(saved, 'totalDebtBalance'), monthlyLoanPayment: draftText(saved, 'monthlyLoanPayment'),
|
||||||
creditCardCount: draftText(saved, 'creditCardCount'), creditCardTotalLimit: draftText(saved, 'creditCardTotalLimit'), creditCardUsedLimit: draftText(saved, 'creditCardUsedLimit'),
|
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'), operatingMonths: draftText(saved, 'operatingMonths'),
|
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 ownershipFromInput(saved: UTSJSONObject, key: string): string {
|
export function ownershipFromInput(saved: UTSJSONObject, key: string): string {
|
||||||
@@ -83,17 +81,16 @@ export function documentsFromInput(saved: UTSJSONObject): SelectedDocument[] {
|
|||||||
})
|
})
|
||||||
return results
|
return results
|
||||||
}
|
}
|
||||||
export function setAssessmentInput(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, editAssessmentId: string = '', baseRevisionId: string = ''): void {
|
||||||
const files: UTSJSONObject[] = documents.map((item): UTSJSONObject => ({ name: item.name, path: item.path }))
|
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 grants: string[] = authorizations.filter((item) => item.selected).map((item) => item.key)
|
||||||
const payload: UTSJSONObject = {
|
const payload: UTSJSONObject = {
|
||||||
type, step, moneyUnit: 'wan', name: form.name, idCard: form.idCard, phone: form.phone, city: form.city, maritalStatus: form.maritalStatus,
|
type, step, editAssessmentId, baseRevisionId, 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,
|
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,
|
house: form.house, car: form.car, savings: form.savings, creditCard: form.creditCard, otherAssets: form.otherAssets,
|
||||||
creditLoanCount: form.creditLoanCount, totalDebtBalance: form.totalDebtBalance, monthlyLoanPayment: form.monthlyLoanPayment,
|
creditLoanCount: form.creditLoanCount, totalDebtBalance: form.totalDebtBalance, monthlyLoanPayment: form.monthlyLoanPayment,
|
||||||
creditCardCount: form.creditCardCount, creditCardTotalLimit: form.creditCardTotalLimit, creditCardUsedLimit: form.creditCardUsedLimit,
|
creditCardCount: form.creditCardCount, creditCardTotalLimit: form.creditCardTotalLimit, creditCardUsedLimit: form.creditCardUsedLimit,
|
||||||
enterpriseName: form.enterpriseName, businessCode: form.businessCode, annualRevenue: form.annualRevenue, operatingYears: form.operatingYears, operatingMonths: form.operatingMonths,
|
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
|
documents: files, authorizations: grants, documentConsent, dataConsent
|
||||||
}
|
}
|
||||||
writeAssessmentFlow(flowToken(), payload)
|
writeAssessmentFlow(flowToken(), payload)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// 登录返回只允许助手路由与编号;拒绝任意returnUrl,重新登录后仍由服务端校验归属。
|
// 登录返回只允许助手路由与编号;拒绝任意returnUrl,重新登录后仍由服务端校验归属。
|
||||||
export function assistantReturnPath(assessmentId: string, productIds: string, chatId: string, from: string): string {
|
export function assistantReturnPath(assessmentId: string, productIds: string, chatId: string, from: string, revisionId: string = ''): string {
|
||||||
const valid = (id: string): boolean => /^[1-9][0-9]{0,9}$/.test(id)
|
const valid = (id: string): boolean => /^[1-9][0-9]{0,9}$/.test(id)
|
||||||
if ((assessmentId.length > 0 && !valid(assessmentId)) || (chatId.length > 0 && !valid(chatId))) return ''
|
if ((assessmentId.length > 0 && !valid(assessmentId)) || (chatId.length > 0 && !valid(chatId))) return ''
|
||||||
const ids = productIds.length == 0 ? [] as string[] : productIds.split(',')
|
const ids = productIds.length == 0 ? [] as string[] : productIds.split(',')
|
||||||
@@ -7,5 +7,6 @@ export function assistantReturnPath(assessmentId: string, productIds: string, ch
|
|||||||
const seen: string[] = []
|
const seen: string[] = []
|
||||||
for (let i = 0; i < ids.length; i++) { if (!valid(ids[i]) || seen.includes(ids[i])) return ''; seen.push(ids[i]) }
|
for (let i = 0; i < ids.length; i++) { if (!valid(ids[i]) || seen.includes(ids[i])) return ''; seen.push(ids[i]) }
|
||||||
if (!['','matches','product','compare','report','next-step','archive'].includes(from)) return ''
|
if (!['','matches','product','compare','report','next-step','archive'].includes(from)) return ''
|
||||||
return '/packages/finance/assistant/assistant?from=' + from + (assessmentId.length > 0 ? '&assessmentId=' + assessmentId + '&productIds=' + productIds : '') + (chatId.length > 0 ? '&chatId=' + chatId : '')
|
if (revisionId.length > 0 && !/^[1-9][0-9]{0,9}$/.test(revisionId)) return ''
|
||||||
|
return '/packages/finance/assistant/assistant?from=' + from + (assessmentId.length > 0 ? '&assessmentId=' + assessmentId + '&productIds=' + productIds : '') + (chatId.length > 0 ? '&chatId=' + chatId : '') + (revisionId.length > 0 ? '&revisionId=' + revisionId : '')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// 登录回跳只接受固定页面和白名单参数,不接受外部 URL;页面仍需重新鉴权。
|
// 登录回跳只接受固定页面和白名单参数,不接受外部 URL;页面仍需重新鉴权。
|
||||||
export function nextStepRoute(assessmentId: string, productId: string = '', section: string = 'actions'): string {
|
export function nextStepRoute(assessmentId: string, productId: string = '', section: string = 'actions', revisionId: string = ''): string {
|
||||||
const valid = (id: string): boolean => /^[1-9][0-9]{0,9}$/.test(id)
|
const valid = (id: string): boolean => /^[1-9][0-9]{0,9}$/.test(id)
|
||||||
if (!valid(assessmentId) || (productId.length > 0 && !valid(productId)) || !['actions', 'prepare'].includes(section)) return ''
|
if (!valid(assessmentId) || (productId.length > 0 && !valid(productId)) || !['actions', 'prepare'].includes(section)) return ''
|
||||||
return '/packages/finance/next-step/next-step?assessmentId=' + assessmentId + (productId.length > 0 ? '&productId=' + productId : '') + '§ion=' + section
|
if (revisionId.length > 0 && !/^[1-9][0-9]{0,9}$/.test(revisionId)) return ''
|
||||||
|
return '/packages/finance/next-step/next-step?assessmentId=' + assessmentId + (productId.length > 0 ? '&productId=' + productId : '') + '§ion=' + section + (revisionId.length > 0 ? '&revisionId=' + revisionId : '')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// 登录后只恢复本人报告的编号和栏目,不接受任意回跳URL。
|
// 登录后只恢复本人报告的编号和栏目,不接受任意回跳URL。
|
||||||
export function reportRoute(assessmentId: string, tab: string = 'overview'): string {
|
export function reportRoute(assessmentId: string, tab: string = 'overview', revisionId: string = ''): string {
|
||||||
if (!/^[1-9][0-9]{0,9}$/.test(assessmentId) || !['overview', 'matches', 'improvements'].includes(tab)) return ''
|
if (!/^[1-9][0-9]{0,9}$/.test(assessmentId) || !['overview', 'matches', 'improvements'].includes(tab)) return ''
|
||||||
return '/packages/finance/report/report?assessmentId=' + assessmentId + '&tab=' + tab
|
if (revisionId.length > 0 && !/^[1-9][0-9]{0,9}$/.test(revisionId)) return ''
|
||||||
|
return '/packages/finance/report/report?assessmentId=' + assessmentId + '&tab=' + tab + (revisionId.length > 0 ? '&revisionId=' + revisionId : '')
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user