feat: 重构评估流程为内存状态管理并接入真实提交与报告接口
本次重构将评估草稿改为内存状态管理,移除本地存储草稿,新增评估流程内存状态模块;接入真实的账户资料获取、头像上传接口,新增账户缓存清理逻辑;重构评估提交逻辑,接入真实提交接口并返回评估ID;新增分析页和报告页的真实状态查询、报告获取功能;更新README文档,记录开发进度和接口状态。
This commit is contained in:
@@ -1,6 +1,8 @@
|
|||||||
<script setup lang="uts">
|
<script setup lang="uts">
|
||||||
import { consumeInitialLaunch } from '@/services/launch.uts'
|
import { consumeInitialLaunch } from '@/services/launch.uts'
|
||||||
|
import { clearLegacyAssessmentCache } from '@/services/cache-lifecycle.uts'
|
||||||
onLaunch((options) => {
|
onLaunch((options) => {
|
||||||
|
clearLegacyAssessmentCache()
|
||||||
if (options.path.length > 0 && options.path != 'pages/home/home') consumeInitialLaunch()
|
if (options.path.length > 0 && options.path != 'pages/home/home') consumeInitialLaunch()
|
||||||
console.log('AI融资已启动')
|
console.log('AI融资已启动')
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
## 本地运行
|
## 本地运行
|
||||||
|
|
||||||
在 HBuilderX 中导入当前目录,选择“运行 → 运行到浏览器”。共享请求配置位于 `services/api.uts`,账户、融资服务分别位于对应分包的 `services/`。现已接入本地后端密码登录、读取资料、退出及原密码改密;短信、贷款申请和 AI 服务尚未接入。
|
在 HBuilderX 中导入当前目录,选择“运行 → 运行到浏览器”。共享请求配置位于 `services/api.uts`,账户、融资服务分别位于对应分包的 `services/`。现已接入本地后端密码登录、账户资料、头像、退出及改密,以及融资需求v2录入、正式提交、历史、DeepSeek自填报告与失败重试;完整度/准备状态及报告v2自填支线已接入;短信与真实产品匹配仍待接入。无草稿;金额显示万元、接口传元字符串。最新状态和验证范围见[开发进度](docs/开发进度.md)0.6及6.6。
|
||||||
|
|
||||||
也可使用 HBuilderX CLI。以下 PowerShell 命令中的安装路径应按本机环境调整;在项目根目录执行:
|
也可使用 HBuilderX CLI。以下 PowerShell 命令中的安装路径应按本机环境调整;在项目根目录执行:
|
||||||
|
|
||||||
@@ -42,18 +42,19 @@ $projectPath = (Get-Location).Path
|
|||||||
|
|
||||||
## 当前前端交互
|
## 当前前端交互
|
||||||
|
|
||||||
个人与企业评估分别保存本机草稿;选图仅供本机预览,资料上传和数据查询尚未接入。课程章节进度、消息已读与体验签到均保存在当前设备,不代表服务器账户数据。
|
个人与企业评估不提供草稿功能:填写页与确认页仅通过内存传递本次输入,离开流程、刷新或退出账户后丢弃;已接入正式提交及历史列表/详情接口;提交后资料只读。选图仅供本机预览,证件资料上传和数据查询尚未接入。课程章节进度、消息已读与体验签到均保存在当前设备,不代表服务器账户数据。
|
||||||
|
|
||||||
个人中心顶部的头像、昵称、手机号区域统一进入修改资料;账户安全独立提供修改密码、验证码重设密码及换绑手机号。昵称和头像缓存按稳定userId隔离,手机号仅保留为内部兼容键。头像编辑交互限制 500KB 以内的 JPG / PNG / WebP 数据,正式上传和持久化仍待开发,不修改融资档案的实名信息。密码登录、原密码改密及退出已接入Redis会话;短信验证和换绑尚未接入,不在本机模拟成功,也不保存密码、验证码或其散列。安全接口成功后清除当前会话并要求重新登录;验证码计时仅在服务端发送成功后启动。
|
个人中心顶部的头像、昵称、手机号区域统一进入修改资料;账户安全独立提供修改密码、验证码重设密码及换绑手机号。昵称和头像缓存按稳定userId隔离,手机号仅保留为内部兼容键。头像已接入后端上传及持久化,限制 500KB 以内的 JPG / PNG / WebP,服务端验证真实图片并重新编码,保存时校验附件归属,不修改融资档案的实名信息。密码登录、原密码改密及退出已接入Redis会话;短信验证和换绑尚未接入,不在本机模拟成功,也不保存密码、验证码或其散列。安全接口成功后清除当前会话并要求重新登录;验证码计时仅在服务端发送成功后启动。
|
||||||
|
|
||||||
账户接口集中在 `packages/account/services/account.uts`,请求携带当前会话的 `Authorization: Bearer <token>`;后端需根据 token 确定账户,校验验证码用途与有效期、限制发送频率并完成旧会话撤销。预留:
|
账户接口集中在 `packages/account/services/account.uts`,请求携带当前会话的 `Authorization: Bearer <token>`;后端需根据 token 确定账户,校验验证码用途与有效期、限制发送频率并完成旧会话撤销。接口状态:
|
||||||
|
|
||||||
- `PUT /account/profile`:`nickname`、`avatar`(图片 data URL 或空字符串);昵称 2–20 字符。当前本地缓存用于个人中心回显,后续应接入服务端资料读取和持久头像存储。
|
- 已实现 `GET/PUT /account/profile`:PUT 接收 `nickname`(2–20字符)及可选 `avatarId`;省略保留原头像,`null` 恢复默认。GET 返回服务端资料。
|
||||||
|
- 已实现 `POST /account/avatar`:multipart 的 `file`,返回 `attachmentId`、`avatarUrl`;头像为公开资料,不能用该接口上传私有证件。
|
||||||
- `POST /account/security/sms-code`:`purpose`(`password` / `phone-current` / `phone-new`)与 `newPhone`(仅新号码验证传入);当前号码从认证账户读取。
|
- `POST /account/security/sms-code`:`purpose`(`password` / `phone-current` / `phone-new`)与 `newPhone`(仅新号码验证传入);当前号码从认证账户读取。
|
||||||
- `PUT /account/password`:`verification`(`password` / `sms`)、`oldPassword` 或 `code`、`newPassword`。新密码为 8–32 位可打印 ASCII,需含字母和数字,不允许空格。
|
- `PUT /account/password`:`verification`(`password` / `sms`)、`oldPassword` 或 `code`、`newPassword`。新密码为 8–32 位可打印 ASCII,需含字母和数字,不允许空格。
|
||||||
- `PUT /account/phone`:`newPhone`、`currentCode`、`newCode`;后端验证两组验证码及新号码占用情况。
|
- `PUT /account/phone`:`newPhone`、`currentCode`、`newCode`;后端验证两组验证码及新号码占用情况。
|
||||||
|
|
||||||
资产与负债包含 `creditLoanCount`(信用贷款笔数)、`totalDebtBalance`(欠款总余额)、`monthlyLoanPayment`(贷款月还款金额)、`creditCardCount`(银行信用卡张数)、`creditCardTotalLimit`(总额度)、`creditCardUsedLimit`(已使用总额度)、`house` / `car`(有 / 无)。移动端所有金额统一以万元输入和展示,最多六位小数;提交接口前精确换算成元的两位小数字符串,范围为 0–999999999.99 元(DECIMAL(11,2))。数量为非负整数;保持原来的可跳过规则,填满八项后计为档案已完善。草稿使用 `moneyUnit: wan`、`draftVersion: 2` 标记;无标记旧草稿的月收入及贷款/信用卡金额从元换算成万元,年营收原本就是万元,保留原值。旧草稿的房车选项自动映射,新数字字段留空,不从旧存款区间或信用卡状态猜测金额;旧字段保留在草稿中以免丢失历史输入。
|
资产与负债包含 `creditLoanCount`(信用贷款笔数)、`totalDebtBalance`(欠款总余额)、`monthlyLoanPayment`(贷款月还款金额)、`creditCardCount`(银行信用卡张数)、`creditCardTotalLimit`(总额度)、`creditCardUsedLimit`(已使用总额度)、`house` / `car`(有 / 无)。移动端所有金额统一以万元输入和展示,最多六位小数;提交接口前精确换算成元的两位小数字符串,范围为 0–999999999.99 元(DECIMAL(11,2))。数量为非负整数;资产负债保持原来的可跳过规则。企业经营时间使用实际经营月数,不从区间猜测。启动时清理旧评估草稿缓存,不导入、不恢复。
|
||||||
|
|
||||||
新闻与课程的数据定义位于 `packages/content/services/content.uts`,产品定义位于 `packages/finance/services/finance.uts`。产品列表与详情共用 ID;预留 `GET /matches`(`items` 数组)及 `GET /products/:id`(单个产品),字段见 `FinanceProduct`,其中视图模型的 `amount` 为万元数值、`rate` 为年化百分比数值。真实接口应返回 `amountYuan`(元的两位小数字符串),由适配层转换;旧 `amount`(万元)仅保留兼容。后端产品表使用独立的额度、期限和利率范围字段,匹配分数与理由属于评估结果。申请入口目前仅引导准备资料,不向金融机构提交申请。
|
新闻与课程的数据定义位于 `packages/content/services/content.uts`,产品定义位于 `packages/finance/services/finance.uts`。产品列表与详情共用 ID;预留 `GET /matches`(`items` 数组)及 `GET /products/:id`(单个产品),字段见 `FinanceProduct`,其中视图模型的 `amount` 为万元数值、`rate` 为年化百分比数值。真实接口应返回 `amountYuan`(元的两位小数字符串),由适配层转换;旧 `amount`(万元)仅保留兼容。后端产品表使用独立的额度、期限和利率范围字段,匹配分数与理由属于评估结果。申请入口目前仅引导准备资料,不向金融机构提交申请。
|
||||||
|
|
||||||
@@ -80,19 +81,40 @@ $projectPath = (Get-Location).Path
|
|||||||
- 微信按 `subPackages` 下载分包。Android、iOS 使用相同目录、页面注册和路由编译为应用,不将小程序按需下载机制等同于 App 热更新。
|
- 微信按 `subPackages` 下载分包。Android、iOS 使用相同目录、页面注册和路由编译为应用,不将小程序按需下载机制等同于 App 热更新。
|
||||||
- 保留现有自绘 `BottomNav`,新闻、消息、我的允许在分包中;不配置要求页面留在主包的原生 `tabBar`。底栏切换使用 `reLaunch`,详情使用 `navigateTo` / `navigateBack`。
|
- 保留现有自绘 `BottomNav`,新闻、消息、我的允许在分包中;不配置要求页面留在主包的原生 `tabBar`。底栏切换使用 `reLaunch`,详情使用 `navigateTo` / `navigateBack`。
|
||||||
- 冷启动首页通过 `services/launch.uts` 只触发一次账户引导,之后按登录状态进入首页或登录;直接打开分包页面和正常返回首页不会重复触发引导。
|
- 冷启动首页通过 `services/launch.uts` 只触发一次账户引导,之后按登录状态进入首页或登录;直接打开分包页面和正常返回首页不会重复触发引导。
|
||||||
- `components/` 与 `services/` 只保留共享组件、布局、请求、会话和跨包草稿。主包或其他分包不得 import 某个分包的专属模块;分包间只通过路由与共享服务交互。
|
- `components/` 与 `services/` 只保留共享组件、布局、请求、会话和当前评估流程的内存状态。主包或其他分包不得 import 某个分包的专属模块;分包间只通过路由与共享服务交互。
|
||||||
- 仅某个分包使用的图片放在该包 `static/`;首页也使用的插图及通用图标保留在根 `static/`,避免分包资源被主包引用。
|
- 仅某个分包使用的图片放在该包 `static/`;首页也使用的插图及通用图标保留在根 `static/`,避免分包资源被主包引用。
|
||||||
|
|
||||||
参考:[uni-app x 页面与分包配置](https://doc.dcloud.net.cn/uni-app-x/collocation/pagesjson.html#subpackages)。
|
参考:[uni-app x 页面与分包配置](https://doc.dcloud.net.cn/uni-app-x/collocation/pagesjson.html#subpackages)。
|
||||||
|
|
||||||
## 2026-09-22 金额与评估存储约定
|
## 2026-09-22 金额与评估存储约定
|
||||||
|
|
||||||
`services/money.uts` 集中处理万元与元的精确转换;确认页与草稿均为万元,`submitAssessment` 仅在提交边界转换。后端每次评估对应一条 `fa_user_info`,其报告和产品快照保存在唯一关联的 `fa_ai_chat` 中。后端数据库及生命周期服务已准备,移动端已切换真实本地接口,未完成的评估、AI、上传及授权接口不会模拟成功,需按开发进度继续接入。
|
`services/money.uts` 集中处理万元与元的精确转换;填写页与确认页均为万元,`submitAssessment` 仅在提交边界转换。后端每次评估对应一条 `fa_user_info`,其报告和产品快照保存在唯一关联的 `fa_ai_chat` 中。后端数据库及生命周期服务已准备,移动端已切换真实本地接口,未完成的评估、AI、私有证件上传及授权接口不会模拟成功,需按开发进度继续接入。
|
||||||
|
|
||||||
|
|
||||||
## 2026-09-22 开发启动状态
|
## 2026-09-22 开发启动状态
|
||||||
|
|
||||||
- 登录默认使用密码模式;需已有手机号账户。尚未配置短信服务商,发送验证码会展示后端503提示,不启动倒计时或生成demo Token。
|
- 登录默认使用密码模式;需已有手机号账户。尚未配置短信服务商,发送验证码会展示后端503提示,不启动倒计时或生成demo Token。
|
||||||
- 会话包含userId/有效期,旧无身份的模拟会话作废。草稿、临时评估状态、昵称头像、课程本机进度、体验签到和消息已读统一经 `services/storage.uts` 按userId隔离。旧设备级草稿保留但不自动分配给登录用户;显式导入和敏感缓存生命周期后续补齐。
|
- 会话包含userId/有效期,旧无身份的模拟会话作废。昵称头像、课程本机进度、体验签到和消息已读按userId隔离;退出、会话过期和401时清理当前账户缓存。评估输入不写本机存储,应用启动清理旧草稿键。
|
||||||
- 首批真实接口不等于25页已业务化。首页/资讯/课程等部分展示仍为原型;评估提交、产品匹配和AI助手接口尚未实现。各模块状态以 [开发进度](docs/开发进度.md)为准。
|
- DeepSeek配置已记录在后端私密环境文件,腾讯云TokenHub地址及两个模型见开发文档;密钥不进入前端,尚未发起真实模型调用。
|
||||||
|
- 首批真实接口不等于25页已业务化。首页/资讯/课程等部分展示仍为原型;正式评估提交与历史已接通,产品匹配和AI助手接口尚未实现。各模块状态以 [开发进度](docs/开发进度.md)为准。
|
||||||
- 默认开发环境不代表正式部署;发布前需核验目标环境、API地址、HTTPS/CORS及所有必做依赖,禁止把本机地址作为生产配置。
|
- 默认开发环境不代表正式部署;发布前需核验目标环境、API地址、HTTPS/CORS及所有必做依赖,禁止把本机地址作为生产配置。
|
||||||
|
|
||||||
|
|
||||||
|
## 2026-09-23 正式评估记录
|
||||||
|
|
||||||
|
- `packages/finance/services/records.uts` 负责表单白名单DTO、元/万元转换、枚举和历史适配;确认页提交获得assessmentId后清空流程内存及页面栈。幂等键在网络失败时复用,不跨刷新保存。
|
||||||
|
- 个人/企业评估提交后保存到后端;分析页读取真实状态,只有processing时每5秒轮询,离开停止;不自动跳到固定分数报告。
|
||||||
|
- 当前分析服务未接通,显示“资料已保存,分析服务尚未开放”;不意味着DeepSeek已经执行。附件/外部授权尚未接入,必须移除本次资料与授权选择后才能提交。
|
||||||
|
- 档案页展示本人历史、分页、错误重试及只读详情;金额万元,身份证/手机号脱敏。无新增页面,仍为25页。
|
||||||
|
|
||||||
|
## 2026-09-23 DeepSeek与分析任务(覆盖此前未接入状态)
|
||||||
|
|
||||||
|
已接腾讯云TokenHub,指定Pro/Flash真实权限通过,默认Flash生成自填资料分析。新增POST `/api/v1/assessments/:id/retry`(HTTP202)及GET `/api/v1/assessments/:id/report`,初次提交HTTP201回执保持不变。前端已接真实状态、幂等失败重试、报告与档案跳转,移除固定评分/雷达;金额精确显示万元,无产品规则时明确未执行匹配。
|
||||||
|
|
||||||
|
后端目录运行 `php think analysis:work`;`--once`只消费一条,`--recover-only`仅补投和回收过期租约。已开启本地私密.env的deepseek.enabled,示例默认false;配置/代码变更后重启worker。开发进程不代表生产守护。
|
||||||
|
|
||||||
|
数据库事务预留任务后投Redis,数据库queued状态支持漏投补偿;running租约过期明确失败,不自动重复调用收费模型。行锁+taskId拦截重复消费和旧结果回写;重试键映射保存原任务回执。fa_ai_chat保存报告和空匹配,fa_ai_chat_log记录用量/状态,未知tokens为null。开发默认每用户每日20次预留、每评估3次;商业预算待确认。
|
||||||
|
|
||||||
|
迁移 `database/migrate_analysis_tasks.py --apply` 已执行,仅增补fa_user_info的task_requests/task_stage/attempt_count/lease_until/dispatched_at及idx_task_dispatch;无新表。执行前0条评估,备份为 `D:/AAdesk/202609/airz-backups/analysis-tasks-20260923-172957-846359/user_info.sql`。回退需先停worker并关闭分析,核对新增业务数据后再用备份恢复,不能直接覆盖后来评估;优先保留兼容字段、回退代码。
|
||||||
|
|
||||||
|
验证:先停全部worker,运行 `php tests/analysis_tasks.php --worker-stopped`(29项)和 `php tests/analysis_http.php --worker-stopped`(15项),使用合成数据与假模型;恢复扫描可能发现任意queued测试行,不能只依赖随机Redis队列隔离。任务测试可显式追加`--live`真实调用一次;`tests/deepseek_smoke.php --live`真实调用两个模型。常规正式评估HTTP回归需分析关闭,开启时会SKIP。当前真实个人自填报告及企业H5重试→worker→报告均通过,普通Chromium与Android编译通过;生产、微信内H5、Safari/真机、外部数据及真实产品匹配未验收。
|
||||||
|
|||||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 249 KiB |
+187
-73
@@ -1,6 +1,6 @@
|
|||||||
# AI 融资平台开发文档
|
# AI 融资平台开发文档
|
||||||
|
|
||||||
> 版本:v1.0 · 基线日期:2026-09-22 · 状态:M0与账户基础已启动,真实短信/AI/产品依赖未就绪
|
> 版本:v1.4 · 基线日期:2026-09-22 · 更新日期:2026-09-24 · 状态:需求输入与报告v2自填支线已实现;完整度/准备状态及执行策略冻结已接入,真实产品匹配待实施
|
||||||
> 配套台账:[开发进度](开发进度.md)。本文定义范围、接口和验收口径,台账记录实际执行与证据。
|
> 配套台账:[开发进度](开发进度.md)。本文定义范围、接口和验收口径,台账记录实际执行与证据。
|
||||||
> 文档初始规划后已开始开发;以下更新记录优先于原始基线。除明确列为已实现的部分外,计划接口和增量数据仍待开发。
|
> 文档初始规划后已开始开发;以下更新记录优先于原始基线。除明确列为已实现的部分外,计划接口和增量数据仍待开发。
|
||||||
|
|
||||||
@@ -8,18 +8,99 @@
|
|||||||
|
|
||||||
- 用户最新决定:删除fa_user_token,会员Token仅存Redis;已备份并删除本地表,当前25张表。fa_user和各业务关系保持不变。运行配置不再依赖MySQL Token驱动。
|
- 用户最新决定:删除fa_user_token,会员Token仅存Redis;已备份并删除本地表,当前25张表。fa_user和各业务关系保持不变。运行配置不再依赖MySQL Token驱动。
|
||||||
- 已接通Bearer、FastAdmin响应解包、业务错误/401处理;新增v1密码登录、本人资料读取、退出、原密码改密。短信接口返回明确未配置状态。
|
- 已接通Bearer、FastAdmin响应解包、业务错误/401处理;新增v1密码登录、本人资料读取、退出、原密码改密。短信接口返回明确未配置状态。
|
||||||
- 前端开发环境指向本地真实接口,按稳定userId隔离本机缓存;测试/生产地址、匿名旧草稿显式导入和敏感缓存清理仍待完成。内容等尚未开发模块仍存在原型数据,不能判一期已完成。
|
- 前端开发环境指向本地真实接口,按稳定userId隔离本机缓存;测试/生产地址、旧草稿已清理且不导入;生产缓存策略仍待验收。内容等尚未开发模块仍存在原型数据,不能判一期已完成。
|
||||||
- Redis驱动使用独立前缀、摘要Token键、TTL和Lua原子索引/撤销。登录和改密按用户行锁串行,Redis故障不降级到MySQL。
|
- Redis驱动使用独立前缀、摘要Token键、TTL和Lua原子索引/撤销。登录和改密按用户行锁串行,Redis故障不降级到MySQL。
|
||||||
- 短信服务商、DeepSeek测试权限、首批真实产品资料目前均未具备;不创建模拟凭证、不伪造AI结果或导入演示产品充当真实资料。
|
- 短信服务商和首批真实产品资料未具备;DeepSeek两个指定模型已通过真实连通性验证,自填资料分析已接入;不创建模拟凭证、不伪造AI结果或导入演示产品充当真实资料。
|
||||||
- 开发任务编号及原69–117人日估算基线保留,尚未填写实际耗时,不凭实施条目推算剩余工期。最新状态与证据见进度文档第0章。
|
- 开发任务编号及原69–117人日估算基线保留,尚未填写实际耗时,不凭实施条目推算剩余工期。最新状态与证据见进度文档第0章。
|
||||||
|
|
||||||
|
### 0.1 最新范围调整与账户实施(2026-09-22)
|
||||||
|
|
||||||
|
- **取消评估草稿功能**:不提供云端创建/更新草稿API,不在本机持久化未提交表单,不提供保存、续填或旧草稿导入。填写页→确认页只传递内存对象;退出账户、离开流程、刷新后清空。正式提交成功才保存fa_user_info及关联评估会话;该接口已于2026-09-23接通,当前边界见0.2。
|
||||||
|
- 账户GET/PUT profile、POST avatar已实现;头像限制500KB,校验真实图片类型和尺寸、重新编码PNG,附件归属校验。昵称和头像从服务端读取,恢复默认头像传avatarId=null。
|
||||||
|
- 退出、过期及401清理对应userId账户缓存及临时表单;启动时清理旧草稿缓存键。其他用户的普通缓存不做全局清空。
|
||||||
|
- fa_user_info仅新增city_code和operating_months标准字段。中途草稿专用revision/client_draft_id及唯一索引已备份撤回,没有新增评估表。原draft状态仅保留为既有内部生命周期兼容,不表示提供用户草稿功能;后续正式提交须在同一事务完成建记录和转processing。
|
||||||
|
- DeepSeek通过[腾讯云TokenHub官方文档](https://cloud.tencent.com/document/product/1823/132248)所述OpenAI兼容接口接入:`https://tokenhub.tencentmaas.com/v1/chat/completions`,Bearer认证。模型按用户指定使用`deepseek-v4-pro-0813`、`deepseek-v4-flash-0731`,不自动更换别名。密钥仅在后端忽略的.env中,application/extra/deepseek.php读取;2026-09-23两个指定模型真实请求通过;Flash已生成个人/企业自填分析,商业预算及供应商余额仍待运营确认。
|
||||||
|
- 任务编号保留,ASSESS-002改为正式评估历史读取。69–117人日保留为原估算基线,取消草稿后尚未重新估算,不将其表述为最新剩余工时。
|
||||||
|
|
||||||
|
|
||||||
|
### 0.2 2026-09-23 正式评估提交与历史查询(当前实现)
|
||||||
|
|
||||||
|
- 新增POST /assessments及GET列表、详情、状态;无新增表、字段或迁移。复用fa_user_info、fa_ai_chat、uk_user_request与现有生命周期。
|
||||||
|
- 同一事务保存完整输入、唯一评估会话和最终状态;在用户行锁内检查幂等键,比较规范化冻结字段(身份证仅内部解密比较,不下发)。响应为HTTP201、`{assessmentId,submissionStatus:"accepted"}`,不把未来变化的taskId或分析状态放入固定提交回执。旧键异内容/已删除记录返回409。
|
||||||
|
- 分析Job/DeepSeek尚未接通时,当前正式记录为failed,errorCode=ANALYSIS_NOT_CONFIGURED,显示“资料已保存,分析服务尚未开放”。这是此前关闭分析的行为;当前报告/retry已接入,匹配仍待开发,最新状态见下文“DeepSeek与可靠任务实施”。
|
||||||
|
- 前端提交前精确把万元换成元字符串,空值转null;后端严格白名单并拒绝数值型金额、超范围或多余字段。文字选项映射稳定枚举;五个现有城市使用已核实代码,自填城市代码为null;经营月数/计数0与未知null分开。
|
||||||
|
- 私有附件及正式外部授权未接通,documents、consentIds当前只接受空数组;选图/勾选仍是本次流程内存,提交时明确阻止而非丢弃。身份证仅格式校验,不代表实名认证。
|
||||||
|
- 历史列表使用id游标,最多50条,默认20条;详情只读且敏感字段脱敏;不存在、内部draft、软删除与越权均不公开。前端具有空态、错误重试、加载更多和按编号刷新详情。
|
||||||
|
- 无草稿原则继续有效;幂等键只存在当前填写流程内存,网络失败重试不换键、内容变化才换键。刷新后未提交内容丢弃;若怀疑上一请求已保存,先查历史,不承诺跨刷新恢复提交键。
|
||||||
|
- 测试和任务证据见开发进度2026-09-23记录;任务剩余工时仍保留原基线,未据此推算完成率。
|
||||||
|
|
||||||
|
## DeepSeek与可靠任务实施(2026-09-23,当前实现)
|
||||||
|
|
||||||
|
- 默认模型为 `deepseek-v4-flash-0731`;`deepseek-v4-pro-0813` 已通过真实连通性验证,可配置选用,不自动回退或换模型。官方协议使用 `thinking:{type:"disabled"}`、`response_format:{type:"json_object"}`,后端Bearer调用。前端不保存密钥。
|
||||||
|
- 正式提交在事务内创建评估、会话及调用预留,事务提交后投Redis队列 `airz-analysis-v1`。队列只携带 assessmentId/taskId,任务事实保存在数据库。stage从queued→running→completed/failed;整体status仍为processing/succeeded/failed。
|
||||||
|
- 新增任务字段均在fa_user_info:task_requests JSON、task_stage、attempt_count、lease_until、dispatched_at及idx_task_dispatch索引。没有新增评估/报告/匹配表。迁移工具 `database/migrate_analysis_tasks.py --apply` 已在本地执行,执行前评估表0行;备份 `D:/AAdesk/202609/airz-backups/analysis-tasks-20260923-172957-846359/user_info.sql`。
|
||||||
|
- running通过行锁和taskId抢占。漏投、Redis丢消息及消费前崩溃由queued扫描补投;执行租约过期记AI_TASK_INTERRUPTED,不自动重复调用收费模型。迟到结果不得覆盖新taskId或报告,但原调用用量仍计入原日志。外部服务未提供幂等保证时,无法承诺重试零重复计费。
|
||||||
|
- 默认开发配额:每用户自然日20次预留,单评估最多3次(首次+2次重试);用户行锁串行化预留,失败也占一次。按PRC时区计日;待定的商业收费/月预算仍属DEC-AI。超时未知tokens记录null,绝不能当作0或免收费。fa_ai_chat_log记录模型快照、状态、tokens、耗时及固定错误码,不保存密钥/供应商错误原文。
|
||||||
|
- 报告输入仅白名单数值事实和个人/企业类型,不发送姓名、手机号、身份证、企业名称、雇主、自由文本或附件。数值由服务端提供,未知字段为null;模型解释只作定性分析。禁止正文复述阿拉伯/全角数字及中文金额、期限、比例,结构/引用/长度不合格即失败。此校验不替代人工评估模型语义。
|
||||||
|
- 当前reportType=self_reported_analysis,overallScore=null;facts每项包含id/label/value:string或null/unit:yuan或month或count/source:self_reported。客户端金额统一精确转换万元。observations引用已知factId;无评分规则时不展示固定分数/雷达。match_result=[]且matchingStatus=unavailable,明确“未执行匹配”,不能误称“匹配后没有产品”。模型和报告内容作为历史快照保存。
|
||||||
|
- 分析页每5秒读取状态,可离开后从档案恢复;失败时提供重试,网络失败复用该次requestId,taskId变化后生成下一键。成功后由用户打开绑定assessmentId的报告。档案和个人中心报告入口均指向真实记录;页面离开清报告,账户切换拒绝旧请求回写。无草稿、无新增页面。
|
||||||
|
|
||||||
|
运行与恢复(在后端目录执行,需PHP Redis/cURL/BCMath/mbstring及MySQL/Redis就绪):
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
php think analysis:work
|
||||||
|
php think analysis:work --once
|
||||||
|
php think analysis:work --recover-only
|
||||||
|
```
|
||||||
|
|
||||||
|
本地私密.env已开启deepseek.enabled;示例配置默认false。修改模型/配置/任务代码需重启常驻worker。开发终端进程不等同生产守护;生产Supervisor/systemd、告警及备份恢复仍由RELEASE/ADMIN验收。关闭开关后新提交仍保存资料,状态为failed/ANALYSIS_NOT_CONFIGURED,可在服务恢复后手动重试。
|
||||||
|
|
||||||
|
新增错误码:AI_TIMEOUT、AI_NETWORK、AI_AUTH、AI_CONFIGURATION、AI_BALANCE、AI_RATE_LIMIT、AI_PROVIDER_ERROR、AI_INVALID_RESPONSE、AI_INVALID_REPORT、AI_TASK_INTERRUPTED、AI_DAILY_LIMIT、AI_ATTEMPT_LIMIT;客户端只显示安全中文信息。401/404维持账户鉴权和归属语义。任务运行失败不改变原始提交的HTTP201固定回执。
|
||||||
|
|
||||||
|
测试:先停全部analysis worker,再执行 `php tests/analysis_tasks.php --worker-stopped`(29项,假模型);`php tests/analysis_http.php --worker-stopped`(15项,随机本机HTTP端口及测试队列)。恢复扫描以数据库为准,仅隔离Redis队列不能阻止常驻worker发现测试行,因此必须停worker;测试会清理合成用户/记录。`--live`仅在任务测试中额外真实调用一次;`php tests/deepseek_smoke.php --live`会真实调用两个模型。原tests/api_assessments.php为分析关闭分支回归,开关开启时主动SKIP,禁止忽略该提示当作通过。
|
||||||
|
|
||||||
|
验证边界:已完成个人合成资料真实分析、企业H5失败重试→队列worker→真实报告→刷新恢复;Chrome普通H5与Android编译通过,不代表微信内H5、Safari、原生真机或生产已验收。真实产品与评分规则、外部查询/私有授权尚未具备,因此REPORT-003/004完整范围保持未验收。
|
||||||
|
|
||||||
|
|
||||||
|
### 0.4 2026-09-23 需求与评估结果契约补充(历史阶段,仅文档)
|
||||||
|
|
||||||
|
已根据用户补充的原型和旧开发说明书,新增[需求与评估结果数据契约](需求与评估结果数据契约.md),作为融资需求、完整度/准备等级、匹配解释三个范围的具体规范。其新增定义优先于本文中同名目标类型的简略说明;“当前实现”段落仍只代表已经运行的v1能力。
|
||||||
|
|
||||||
|
- 新输入inputVersion=2,计划补requestedAmountYuan、requestedTermMonths、purposeCode、purposeDetail;数据库存元的DECIMAL(11,2),客户端万元,不恢复草稿。
|
||||||
|
- 完整度由版本化字段清单计算;准备等级是资料状态,规则需DEC-READINESS发布。未发布不展示分数或等级;与授信/审批概率严格区分。
|
||||||
|
- 产品硬准入pass/fail/unknown合并为eligible/ineligible/unknown,保留证据;score可空,理由按事实最多展示三条,不凑数。未执行、零候选、执行失败分别表达。
|
||||||
|
- 结果resultVersion=2计划新增,v1历史不重算;提交事务冻结输入/用途字典依据,worker首次抢占后、外部调用前补入计算策略/产品快照,重试复用。新输入列及fa_ai_chat.analysis_context仅为迁移设计,本次没有执行迁移。
|
||||||
|
- 新增DEC-DEMAND、DEC-READINESS决策项;DEC-MATCH继续确认产品规则与排序。字段字典建议、完整度字段集合和准备等级建议尚未发布,不写成已确认商业规则。
|
||||||
|
- 本次仅补文档,相关业务子项均未开始。保持原60个任务编号并同步扩展范围;原工时为历史基线,增量待重估。本次不自动纳入渠道、注销、RAG、对比或PDF开发。
|
||||||
|
|
||||||
|
### 0.5 2026-09-24 融资需求v2实施(历史阶段,报告版本以0.6为准)
|
||||||
|
|
||||||
|
- DEC-DEMAND按用户授权采用契约用途字典及必填规则,已发布 `financing-purposes-20260923-v1`。个人/企业在原第2步录入金额、整数月期限、资金用途及other说明;确认页独立展示,历史详情按冻结标签回显,旧v1显示“当时未采集”。无草稿,刷新/离开清空未提交输入。
|
||||||
|
- GET `/api/v1/dictionaries` 已公开实现,返回支持输入版本、最低新建版本、带版本的用途项;响应no-store。当前只发布融资用途及输入版本,不宣称地区等全部字典已动态化。配置位于后端 `application/extra/assessment.php`;修改用途须提升字典版本,移除条目即停止新建使用,旧回放读取冻结内容;后台编辑尚未实现。
|
||||||
|
- POST `/assessments` 新建最低inputVersion=2。需求金额使用元两位十进制字符串(0.01—999999999.99),前端万元字符串精确转换;期限1—9999整数月。用途按个人/企业区分;other说明trim后2—200字,非other只接受null/缺省;信用卡已用大于总额返回422 FIELD_INCONSISTENT。
|
||||||
|
- 已有请求先按原版本和冻结字典比较:同内容HTTP201原回执,合法内容或版本改变409,非法格式422;v1原键可重放,新v1键422 INPUT_VERSION_UNSUPPORTED。详情只返回本人脱敏资料;用途字典/哈希失效明确503,不套当前配置猜历史含义。
|
||||||
|
- 本地2026-09-23已执行 `database/migrate_assessment_demand.py --apply`:fa_user_info增加input_version及需求四列,fa_ai_chat增加analysis_context,新增3个CHECK;共9项变更,无新表。迁移前两表均0行,复跑预检0项DDL。恢复点:`D:/AAdesk/202609/airz-backups/assessment-demand-20260923-195131-493172/before.sql`。默认只预检,禁止把备份覆盖后续业务数据;优先保留兼容列回退代码。
|
||||||
|
- 提交事务冻结 `input_snapshot={inputVersion,normalizationVersion,purposeDictionary}`,字典保存完整内容,规范排序JSON后计算inputHash。基础字段校验仍由固定normalizationVersion代码解释,未冻结全部字段规则全文;后续修改必须保留旧版本处理。execution_snapshot、executionHash、contentHash当前为null,执行策略/产品快照仍待REPORT-002实施。
|
||||||
|
- 输入版本与报告版本独立:新v2输入继续生成resultVersion=1自填报告。本轮需求未进入模型事实、准备等级或产品匹配;自由用途说明不外发。REPORT-003/004及DEC-READINESS/DEC-MATCH保持未完成,不把本次录入闭环称为报告v2。
|
||||||
|
- 验证:2026-09-24专项76项通过;2026-09-23基础41项、任务29项、隔离HTTP15项通过。个人/企业H5提交及历史刷新、金额精度、旧未采集、用途失败恢复与无草稿已验证;Web/Android本地编译通过,未验收微信内H5、Safari或真机。此次未调用真实模型,完整证据见开发进度0.5与6.5。
|
||||||
|
|
||||||
|
### 0.6 2026-09-24 资料完整度、准备状态与报告v2(当前实现)
|
||||||
|
|
||||||
|
- 根据用户继续下一步的授权,采用专项契约首版等权字段集和三种资料准备状态;DEC-READINESS开发版发布为 `self-reported-base-20260924-v1`。个人14项、企业9项,other各加1;有效零值/none计已填写。缺失、无效、不适用分开记录,按整数四舍五入计算百分比;invalid或一致性问题优先“需核实资料”,其次“待完善资料”,全部填写为“可进入资料核对”。这是自填资料准备状态,不是信用或授信等级。
|
||||||
|
- 策略配置在 `application/extra/readiness.php`,引擎版本 `equal-fields-v1` 固定定义字段类型、范围、条件项、计数和优先级;字段定义、适用清单、条件规则、标签及版本完整复制至execution_snapshot。变更业务含义须提升策略版本;变更算法/类型须提升引擎版本并保留旧版处理,不能原地改变历史策略含义。
|
||||||
|
- 新提交input_snapshot冻结targetResultVersion=2;升级前缺该标记或标记1的评估继续生成v1报告,已有成功报告不重算。本轮没有迁移或改写原业务记录。首次worker抢占后、外部调用前持有评估行锁,写入执行策略、evaluatedAt、目标报告版本和明确的匹配unavailable原因,生成executionHash/contentHash;失败重试沿用原快照。策略异常/分母为0报ASSESSMENT_POLICY_INVALID,快照损坏报ASSESSMENT_CONTEXT_UNAVAILABLE,均在模型调用前失败。
|
||||||
|
- GET `/assessments/:id/report` 现按resultVersion支持v1/v2;v2为financing_needs_analysis,包含demand、带类型/单位/来源的facts、completeness、readiness、consistencyIssues、matching、policyVersions以及AI定性区块。服务端固定提示另存systemWarnings:string[],不混作AI证据。completeness.items额外提供label;事实可带state,异常原值保留评估资料、报告value置null并显示需核实。
|
||||||
|
- DeepSeek v2仅接数值/枚举白名单事实与确定性准备依据,不发姓名、证件、手机、企业名、城市/职业自由文本或用途说明。仅接受summary/strengths/improvements/riskWarnings四个模型字段;数组条目必须引用已外发的fact或readiness依据,拒绝无效/重复/空引用、数值改写、HTML及明确审批承诺。模型无权覆盖金额、完整度和准备状态;语义正确性仍需真实模型与人工样例验收。
|
||||||
|
- 报告页已展示需求、完整度逐项状态、准备依据、优势/建议/风险及证据标签;金额万元,期限/月数整数。未发布策略不显示0分,旧v1不事后添分。缺少真实产品时matching.status=unavailable、计数null、match_result=[];产品集合/匹配规则/排序快照仅保留明确不可用状态,真实匹配和产品解释仍待PRODUCT实施。
|
||||||
|
- 本轮验证:54项报告专项、76项需求、41项基础、15项HTTP检查通过;Chromium手机/桌面报告和历史刷新通过,Web及Android本地编译通过。用合成数据/假模型,未调用真实DeepSeek;v2提示词真实语义、微信内H5、Safari、真机和生产守护未验收。REPORT-002/004整体开发中,REPORT-003完整产品/外部数据范围仍阻塞,不能把内部报告支线记为一期完成。
|
||||||
|
|
||||||
## 1. 项目目标与现状
|
## 1. 项目目标与现状
|
||||||
|
|
||||||
### 1.1 已确认范围
|
### 1.1 已确认范围
|
||||||
|
|
||||||
一期将现有 **25 个页面全部接入真实业务,H5 优先首发**。包含手机号/密码/微信/一键登录、个人与企业评估、私有资料、正式授权、外部征信或数据查询、产品匹配、DeepSeek 报告、历史档案、AI 助手、新闻、图文课堂、搜索、消息、积分邀请、帮助反馈及人工客服。相关运营后台一并验收。
|
一期将现有 **25 个页面全部接入真实业务,H5 优先首发**。包含手机号/密码/微信/一键登录、个人与企业评估、私有资料、正式授权、外部征信或数据查询、产品匹配、DeepSeek 报告、历史档案、AI 助手、新闻、图文课堂、搜索、消息、积分邀请、帮助反馈及人工客服。相关运营后台一并验收。
|
||||||
|
|
||||||
按 **1 人全栈+AI 辅助**排期。AI 服务商确定为 **DeepSeek**;具体模型、预算和测试权限待核验。短信、号码认证、外部数据、客服、存储等渠道待选型。微信内网页与普通 H5 的登录方式分别核实平台能力。一期必做渠道未接通时保持未验收,不以模拟成功替代。
|
按 **1 人全栈+AI 辅助**排期。AI 服务商确定为 **DeepSeek**;模型为deepseek-v4-pro-0813和deepseek-v4-flash-0731,密钥已收妥;真实权限已验证,预算和可用余额待运营确认。短信、号码认证、外部数据、客服、存储等渠道待选型。微信内网页与普通 H5 的登录方式分别核实平台能力。一期必做渠道未接通时保持未验收,不以模拟成功替代。
|
||||||
|
|
||||||
微信小程序、Android、iOS 的平台适配、真机、签名和发布列入后续里程碑,不计入本期 69–117 人日。保留现有跨端代码结构。贷款功能止于申请准备和资料引导;当前范围不包含金融机构受理、授信、放款、支付或资金交易。课堂保持图文形式。
|
微信小程序、Android、iOS 的平台适配、真机、签名和发布列入后续里程碑,不计入本期 69–117 人日。保留现有跨端代码结构。贷款功能止于申请准备和资料引导;当前范围不包含金融机构受理、授信、放款、支付或资金交易。课堂保持图文形式。
|
||||||
|
|
||||||
@@ -30,9 +111,9 @@
|
|||||||
| 前端 | uni-app x / Vue 3 / UTS,25 页,无独立 npm 构建入口 | 沿用 HBuilderX 和现有分包,不重新搭建前端 |
|
| 前端 | uni-app x / Vue 3 / UTS,25 页,无独立 npm 构建入口 | 沿用 HBuilderX 和现有分包,不重新搭建前端 |
|
||||||
| 后端 | FastAdmin / ThinkPHP 5.0.28 / PHP 7.4 | 复用认证、后台、模型与队列依赖,新增业务 API/服务 |
|
| 后端 | FastAdmin / ThinkPHP 5.0.28 / PHP 7.4 | 复用认证、后台、模型与队列依赖,新增业务 API/服务 |
|
||||||
| 数据库 | MySQL 8.0.44,删除空 Token 表后实际 25 张 InnoDB 表 | 已核对本地库并更新结构快照;恢复点见进度第0节 |
|
| 数据库 | MySQL 8.0.44,删除空 Token 表后实际 25 张 InnoDB 表 | 已核对本地库并更新结构快照;恢复点见进度第0节 |
|
||||||
| 当前前端模式 | 开发环境已连接本地 v1 API,模拟请求开关关闭 | 密码账户流程已接入,内容与报告等页面仍有原型数据,不按页面数计算完成度 |
|
| 当前前端模式 | 开发环境已连接本地 v1 API,模拟请求开关关闭 | 密码账户流程已接入,内容与产品匹配等页面仍有原型数据,不按页面数计算完成度 |
|
||||||
| 当前后端能力 | 评估生命周期、加密、密码兼容、产品后台等基础已存在 | 内部服务不等于已暴露并联调的业务 API |
|
| 当前后端能力 | 评估生命周期、加密、密码兼容、产品后台等基础已存在 | 内部服务不等于已暴露并联调的业务 API |
|
||||||
| 第三方接入 | 尚未验证真实短信、DeepSeek、外部查询、客服闭环 | 各渠道独立记录测试环境与验收证据 |
|
| 第三方接入 | DeepSeek自填分析已真实验证;短信、外部查询、客服闭环未验收 | 各渠道独立记录测试环境与验收证据 |
|
||||||
|
|
||||||
源代码位置:
|
源代码位置:
|
||||||
|
|
||||||
@@ -42,14 +123,14 @@
|
|||||||
|
|
||||||
### 1.3 已有基础与首要缺口
|
### 1.3 已有基础与首要缺口
|
||||||
|
|
||||||
已有基础:万元与元精确转换、个人/企业草稿交互、产品范围字段和版本、评估提交/完成/失败/重试内部服务、敏感字段加密、旧密码兼容、Token Redis切换(早期曾恢复表,现已删除)、日志脱敏及基础验证。
|
已有基础:万元与元精确转换、个人/企业分步填写交互、产品范围字段和版本、评估提交/完成/失败/重试内部服务、敏感字段加密、旧密码兼容、Token Redis切换(早期曾恢复表,现已删除)、日志脱敏及基础验证。
|
||||||
|
|
||||||
首要缺口:
|
首要缺口:
|
||||||
|
|
||||||
1. Bearer、FastAdmin响应解析和401处理已接通;全量字段字典、requestId日志关联、生产环境配置仍待完成。
|
1. Bearer、FastAdmin响应解析和401处理已接通;全量字段字典、requestId日志关联、生产环境配置仍待完成。
|
||||||
2. 确认页未承接评估编号,分析页计时跳转,报告使用固定分数;必须打通 assessmentId 全链路。
|
2. assessmentId已贯穿提交、真实分析、报告和历史;需求金额/期限/用途、完整度/准备等级及确定性产品匹配仍待按新增契约实现。
|
||||||
3. 本机草稿、学习进度、签到和消息状态已按userId隔离;旧匿名草稿显式导入、退出后敏感缓存清理与云端持久化仍待完成。
|
3. 账户缓存已按userId隔离并在退出时清理;评估只使用当前流程内存。学习进度、签到和消息的服务端业务仍待接入。
|
||||||
4. 产品准入、业务 API、评估队列、DeepSeek 请求与真实报告均待开发。
|
4. 产品准入和其他业务API待开发;评估队列、DeepSeek自填报告及读取API已接通。
|
||||||
5. 通用公共上传不能直接存正式证件;授权勾选和插件钩子不代表第三方服务已接入。
|
5. 通用公共上传不能直接存正式证件;授权勾选和插件钩子不代表第三方服务已接入。
|
||||||
|
|
||||||
## 2. 页面与功能覆盖
|
## 2. 页面与功能覆盖
|
||||||
@@ -67,7 +148,7 @@
|
|||||||
| 7 | packages/finance/matches/matches | 当前评估匹配、排序、无结果说明 | PRODUCT-004 |
|
| 7 | packages/finance/matches/matches | 当前评估匹配、排序、无结果说明 | PRODUCT-004 |
|
||||||
| 8 | packages/finance/product/product | 产品范围、快照、当前可用性与申请准备 | PRODUCT-003、PRODUCT-004 |
|
| 8 | packages/finance/product/product | 产品范围、快照、当前可用性与申请准备 | PRODUCT-003、PRODUCT-004 |
|
||||||
| 9 | packages/finance/assistant/assistant | 持久会话、报告解读 | CHAT-001、CHAT-002、CHAT-003、CHAT-004 |
|
| 9 | packages/finance/assistant/assistant | 持久会话、报告解读 | CHAT-001、CHAT-002、CHAT-003、CHAT-004 |
|
||||||
| 10 | packages/finance/archive/archive | 草稿、评估历史、复制新评估 | ASSESS-002、ASSESS-004、REPORT-004 |
|
| 10 | packages/finance/archive/archive | 已提交评估历史、开始新评估 | ASSESS-002、ASSESS-004、REPORT-004 |
|
||||||
| 11 | packages/account/index/index | 启动引导及登录回跳 | BASE-003、BASE-004 |
|
| 11 | packages/account/index/index | 启动引导及登录回跳 | BASE-003、BASE-004 |
|
||||||
| 12 | packages/account/login/login | 手机号、密码、微信、一键登录 | AUTH-001、AUTH-002、LOGIN-001、LOGIN-002、LOGIN-003、LOGIN-004 |
|
| 12 | packages/account/login/login | 手机号、密码、微信、一键登录 | AUTH-001、AUTH-002、LOGIN-001、LOGIN-002、LOGIN-003、LOGIN-004 |
|
||||||
| 13 | packages/account/profile/profile | 用户信息、档案、入口和退出 | AUTH-002、AUTH-003、ASSESS-004 |
|
| 13 | packages/account/profile/profile | 用户信息、档案、入口和退出 | AUTH-002、AUTH-003、ASSESS-004 |
|
||||||
@@ -94,8 +175,8 @@
|
|||||||
- fa_ai_chat_log 的角色为 system/user/assistant/tool;状态为 pending/streaming/completed/failed。会话内序号唯一;(chat_id,request_id,role) 保证消息级幂等。一期普通助手默认异步完整回复,不要求流式传输。
|
- fa_ai_chat_log 的角色为 system/user/assistant/tool;状态为 pending/streaming/completed/failed。会话内序号唯一;(chat_id,request_id,role) 保证消息级幂等。一期普通助手默认异步完整回复,不要求流式传输。
|
||||||
- 评估状态为 draft → processing → succeeded 或 failed;仅 failed 可重试并进入 processing。填写步骤 step=0–4 与状态分离。排队、数据查询、匹配、生成报告属于任务阶段,不擅自加入现有 status 枚举。
|
- 评估状态为 draft → processing → succeeded 或 failed;仅 failed 可重试并进入 processing。填写步骤 step=0–4 与状态分离。排队、数据查询、匹配、生成报告属于任务阶段,不擅自加入现有 status 枚举。
|
||||||
- requestId 使用 8–64 位字母、数字、下划线或横线,作为用户内提交幂等键。taskId 为服务端每次尝试生成的标识;重试更换,旧任务禁止覆盖新任务。
|
- requestId 使用 8–64 位字母、数字、下划线或横线,作为用户内提交幂等键。taskId 为服务端每次尝试生成的标识;重试更换,旧任务禁止覆盖新任务。
|
||||||
- 当前 submit/complete/fail/retry 已有内部实现;submit重复调用现返回当前taskId,尚不能代表稳定的原尝试响应。计划在评估运行元数据task_requests中持久保存每个提交/重试键、规范化参数摘要、原taskId与接受响应;重复旧键只返回原响应,不启动新任务。写映射与状态变更同一事务锁定评估行,最新状态另查status接口。草稿并发版本、任务阶段与可靠派发仍需补充。
|
- 内部submit/complete/fail/retry只供服务端使用;外部提交返回固定assessmentId/submissionStatus,重试通过task_requests保存每个键的原taskId与回执。初次提交比较规范化冻结输入,同键异内容409;重试仅接受requestId。映射和状态变更同事务锁定用户/评估行,最新状态另查status。可靠派发、租约及失败恢复已接入,不提供用户草稿版本。
|
||||||
- result_version 是结果 JSON 结构版本,rule_version 是规则版本字符串,product.version 是产品整数版本。当前 complete 尚未显式写入 result_version,REPORT-003 补齐。
|
- result_version 是结果 JSON 结构版本,rule_version 是规则版本字符串,product.version 是产品整数版本。当前分析任务已显式写入 result_version=1;产品快照部分随REPORT-003继续补齐。
|
||||||
- 外部查询结果不回写已提交表单。使用单独查询流水保存来源、授权和标准化结果,任务在生成报告前冻结所用结果集合/截止时间;最终报告保存引用、来源及必要摘要。后到结果不得悄悄重写历史报告。
|
- 外部查询结果不回写已提交表单。使用单独查询流水保存来源、授权和标准化结果,任务在生成报告前冻结所用结果集合/截止时间;最终报告保存引用、来源及必要摘要。后到结果不得悄悄重写历史报告。
|
||||||
|
|
||||||
### 3.2 金额、产品与数据字典
|
### 3.2 金额、产品与数据字典
|
||||||
@@ -109,7 +190,7 @@
|
|||||||
| 期限 | 整数月;产品 term_min_months/term_max_months |
|
| 期限 | 整数月;产品 term_min_months/term_max_months |
|
||||||
| 评估类型 | API personal/enterprise → DB type=1/2 |
|
| 评估类型 | API personal/enterprise → DB type=1/2 |
|
||||||
| 婚姻 | API已定义枚举 → DB 1已婚、2未婚、3离异、4丧偶;未知NULL |
|
| 婚姻 | API已定义枚举 → DB 1已婚、2未婚、3离异、4丧偶;未知NULL |
|
||||||
| 房/车 | API布尔值或null → DB house/vehicle=1/0/NULL;旧“有/无”在适配层转换 |
|
| 房/车 | 当前API yes/no/null → DB house/vehicle=1/0/NULL;界面“有/无”在适配层转换 |
|
||||||
| 产品对象 | DB object 为 SET,可同时适用于个人和企业;不是单一类型值 |
|
| 产品对象 | DB object 为 SET,可同时适用于个人和企业;不是单一类型值 |
|
||||||
| 产品状态 | switch=0草稿、1上架、2下架;还需检查有效期 |
|
| 产品状态 | switch=0草稿、1上架、2下架;还需检查有效期 |
|
||||||
| 地区 | 使用统一版本的地区代码;regions=[]才表示全国,NULL视为未配置并阻止上架 |
|
| 地区 | 使用统一版本的地区代码;regions=[]才表示全国,NULL视为未配置并阻止上架 |
|
||||||
@@ -128,32 +209,38 @@
|
|||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| type | fa_user_info.type | personal/enterprise转1/2 |
|
| type | fa_user_info.type | personal/enterprise转1/2 |
|
||||||
| name / idCard / phone | name / card_no / mobile | card_no加密;只向所属用户按必要范围回显 |
|
| name / idCard / phone | name / card_no / mobile | card_no加密;只向所属用户按必要范围回显 |
|
||||||
| city / regionCode | city / 拟增region_code | 保留展示名,匹配使用标准代码;不恢复fa_area |
|
| city / cityCode | city / city_code(已增) | 保留展示名,匹配使用标准代码;不恢复fa_area |
|
||||||
| maritalStatus | marriage | 文字转稳定枚举,再映射数字 |
|
| maritalStatus | marriage | 文字转稳定枚举,再映射数字 |
|
||||||
| occupation / company | profession / employer | 企业occupation表示行业 |
|
| occupation / company | profession / employer | 企业occupation表示行业 |
|
||||||
| monthlyIncome / annualRevenue | monthly_income / year_income | 元字符串;分别用于个人/企业 |
|
| monthlyIncome / annualRevenue | monthly_income / year_income | 元字符串;分别用于个人/企业 |
|
||||||
| socialSecurity / housingFund | social_security / housing_provident_fund | 统一枚举,区分无、未知与未填写 |
|
| socialSecurity / housingFund | social_security / housing_provident_fund | 统一枚举,区分无、未知与未填写 |
|
||||||
| house / car | house / vehicle | 布尔值或null |
|
| house / car | house / vehicle | 当前API yes/no/null;不混用布尔值 |
|
||||||
| creditLoanCount / totalDebtBalance | credit_num / credit_total_money | 笔数整数,金额元 |
|
| creditLoanCount / totalDebtBalance | credit_num / credit_total_money | 笔数整数,金额元 |
|
||||||
| monthlyLoanPayment | credit_month_still_money | 元 |
|
| monthlyLoanPayment | credit_month_still_money | 元 |
|
||||||
| creditCardCount / creditCardTotalLimit / creditCardUsedLimit | credit_card_num / credit_card_quota / credit_card_use_quota | 已用不得大于总额;未知不填0 |
|
| creditCardCount / creditCardTotalLimit / creditCardUsedLimit | credit_card_num / credit_card_quota / credit_card_use_quota | 已用不得大于总额;未知不填0 |
|
||||||
| enterpriseName / businessCode | enterprise_name / business_code | 企业名称、统一社会信用代码 |
|
| enterpriseName / businessCode | enterprise_name / business_code | 企业名称、统一社会信用代码 |
|
||||||
| operatingMonths | 拟增operating_months | 保留旧operating_years供兼容,稳定准入只用规范月数 |
|
| operatingMonths | operating_months(已增) | 保留旧operating_years供兼容,稳定准入只用规范月数 |
|
||||||
|
| inputVersion | 已增input_version,默认1 | 已实现v2;历史保持1,新建版本与幂等回放分开处理 |
|
||||||
|
| requestedAmountYuan | 已增requested_amount DECIMAL(11,2) NULL | v2必填,元字符串且>0;不是负债或收入 |
|
||||||
|
| requestedTermMonths | 已增requested_term_months SMALLINT UNSIGNED NULL | v2必填整数月1—9999,产品规则另行限制 |
|
||||||
|
| purposeCode / purposeDetail | 已增purpose_code / purpose_detail | 已发布版本化用途字典,other时才要求说明;自由说明不进入AI/规则 |
|
||||||
| documents | documents JSON | 类型、attachmentId、元信息;不接受name/path临时文件作为持久资料 |
|
| documents | documents JSON | 类型、attachmentId、元信息;不接受name/path临时文件作为持久资料 |
|
||||||
| consentIds | consent_data快照 | 服务端读取授权事件并形成快照,不相信客户端时间或协议正文 |
|
| consentIds | consent_data快照 | 服务端读取授权事件并形成快照,不相信客户端时间或协议正文 |
|
||||||
| savings / creditCard / otherAssets | 拟增supplementary_data JSON | 保存旧表单补充输入,标明来源和版本,不据此猜测精确金额或准入事实 |
|
| savings / creditCard / otherAssets | 拟增supplementary_data JSON | 保存旧表单补充输入,标明来源和版本,不据此猜测精确金额或准入事实 |
|
||||||
| step / draftVersion | step / 拟增draft_version | step控制续填;draft_version为服务端乐观锁,区别本机格式draftVersion=2 |
|
| step | step | 仅当前页面步骤;不保存草稿版本或续填进度 |
|
||||||
|
|
||||||
本机格式标记 moneyUnit=wan、draftVersion=2 继续用于旧草稿规范化;服务器修订字段在API命名为 revision,避免混淆。提交与存储DTO只接受 moneyUnit=yuan。
|
当前流程金额以moneyUnit=wan表示,正式提交与数据库使用moneyUnit=yuan。旧草稿不恢复,无revision契约。
|
||||||
|
|
||||||
### 3.4 增量数据设计工作
|
### 3.4 增量数据设计工作
|
||||||
|
|
||||||
以下为待实现的最小数据方案,由对应任务提交增量迁移和回滚说明。本次未创建任何表或字段。所有新增关系都采用服务端所有者校验;必要唯一索引以数据库保证并发一致性。
|
以下按已实现和计划增量区分;本轮已补city_code、operating_months,其他增量仍待对应任务实施。所有新增关系都采用服务端所有者校验;必要唯一索引以数据库保证并发一致性。
|
||||||
|
|
||||||
| 需求 | 复用位置 | 计划增量与约束 | 负责任务 |
|
| 需求 | 复用位置 | 计划增量与约束 | 负责任务 |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| 草稿、标准字段、补充输入 | fa_user_info | region_code、operating_months、supplementary_data、draft_version、create_request_id;用户+create_request_id唯一,对应clientDraftId;提交后输入冻结 | ASSESS-001、ASSESS-002 |
|
| 标准字段、补充输入 | fa_user_info | city_code、operating_months已新增;supplementary_data按正式提交字段需求补充;不增加草稿版本或创建草稿键;正式提交后输入冻结 | ASSESS-001、ASSESS-002 |
|
||||||
| 任务可靠执行 | fa_user_info及现有生命周期 | task_requests JSON持久保存每次提交/重试键到参数摘要、原taskId和接受响应的映射;补task_stage、attempt_count、lease_until,按task_id抢占与失联补偿;运行元数据不改已冻结输入 | ASSESS-003、REPORT-002 |
|
| 任务可靠执行 | fa_user_info及现有生命周期 | task_requests JSON保存重试键→原taskId及接受回执;初次提交仍用request_id和冻结输入比较;task_stage、attempt_count、lease_until、dispatched_at已补齐,按task_id抢占和补投;不改已冻结输入 | ASSESS-003、REPORT-002 |
|
||||||
|
| 融资需求v2 | fa_user_info | input_version、requested_amount、requested_term_months、purpose_code、purpose_detail;历史需求NULL;新版本与原键幂等回放见专项契约第7节 | ASSESS-001、ASSESS-003 |
|
||||||
|
| 输入/计算依据快照与报告v2 | fa_ai_chat | 已增analysis_context JSON并冻结input_snapshot;新目标v2首次抢占后且外部调用前已补入execution_snapshot;真实产品/匹配快照仍待接,两个部分均不可覆盖;report_data、match_result仍复用原字段,不新建报告表 | ASSESS-003、REPORT-002、REPORT-003、PRODUCT-002 |
|
||||||
| 私有附件 | fa_attachment | 存储可见性、文件用途、存储键、生命周期状态;路径不作为权限依据 | FILE-001、FILE-002 |
|
| 私有附件 | fa_attachment | 存储可见性、文件用途、存储键、生命周期状态;路径不作为权限依据 | FILE-001、FILE-002 |
|
||||||
| 协议与授权事件 | fa_acticle + fa_user_info.consent_data | 协议版本不可变修订;拟增fa_user_consent保存范围、版本、授权与撤销事件,再复制评估快照 | FILE-003 |
|
| 协议与授权事件 | fa_acticle + fa_user_info.consent_data | 协议版本不可变修订;拟增fa_user_consent保存范围、版本、授权与撤销事件,再复制评估快照 | FILE-003 |
|
||||||
| 第三方身份 | fa_user + token | 拟增fa_user_identity,以provider+app_id+subject唯一;不能只靠单个openid列覆盖所有微信应用 | LOGIN-001 |
|
| 第三方身份 | fa_user + token | 拟增fa_user_identity,以provider+app_id+subject唯一;不能只靠单个openid列覆盖所有微信应用 | LOGIN-001 |
|
||||||
@@ -167,9 +254,13 @@
|
|||||||
|
|
||||||
变更只追加有明确业务目的的字段或关系。新增这些支撑表不改变“不新增独立评估、报告、匹配表”的约定。详细DDL在对应数据任务内提交审阅;已有数据需先预检、备份、回填验证,再收紧约束。不能直接重跑原空业务表限定的迁移脚本。
|
变更只追加有明确业务目的的字段或关系。新增这些支撑表不改变“不新增独立评估、报告、匹配表”的约定。详细DDL在对应数据任务内提交审阅;已有数据需先预检、备份、回填验证,再收紧约束。不能直接重跑原空业务表限定的迁移脚本。
|
||||||
|
|
||||||
|
### 3.5 需求与评估结果详细契约
|
||||||
|
|
||||||
|
融资需求字段及校验、完整度字段集和公式、准备等级发布闸门、规则证据、三态结果、报告v2及历史迁移以[专项契约](需求与评估结果数据契约.md)第2—8节为准。需求输入、完整度、准备状态及报告v2自填支线已实现;真实产品匹配及其证据仍为计划增量。输入版本、结果版本、业务策略与产品版本分别保存。
|
||||||
|
|
||||||
## 4. 模块开发任务
|
## 4. 模块开发任务
|
||||||
|
|
||||||
任务编号与[开发进度](开发进度.md)一一对应;每个模块4项,共60项。工时为剩余工程人日,含模块内开发者测试,不含外部审核等待。任务级验收是具体交付条件,模块整体还需通过M4的真实联调。
|
任务编号与[开发进度](开发进度.md)一一对应;每个模块4项,共60项。工时保留初始工程人日基线(本轮取消草稿后尚待重估),含模块内开发者测试,不含外部审核等待。任务级验收是具体交付条件,模块整体还需通过M4的真实联调。
|
||||||
|
|
||||||
|
|
||||||
### 4.1 BASE|基础接口与环境
|
### 4.1 BASE|基础接口与环境
|
||||||
@@ -179,7 +270,7 @@
|
|||||||
- **现状:** 页面、导航、安全区已有;共享请求已接本地API,处理Bearer、业务失败与401;全量契约、requestId及生产配置仍待完成。
|
- **现状:** 页面、导航、安全区已有;共享请求已接本地API,处理Bearer、业务失败与401;全量契约、requestId及生产配置仍待完成。
|
||||||
- **前端:** 统一请求与账户专用请求,处理认证、业务错误、重复点击、加载/空态、401回跳;首页聚合公共内容与登录后状态。
|
- **前端:** 统一请求与账户专用请求,处理认证、业务错误、重复点击、加载/空态、401回跳;首页聚合公共内容与登录后状态。
|
||||||
- **后端:** 新路由显式绑定控制器和方法;解析JSON并逐字段校验,不能对结构化输入机械套用HTML转义;兼容旧Token入口但新客户端只发送Bearer。
|
- **后端:** 新路由显式绑定控制器和方法;解析JSON并逐字段校验,不能对结构化输入机械套用HTML转义;兼容旧Token入口但新客户端只发送Bearer。
|
||||||
- **数据:** 以userId建立缓存命名空间。旧匿名草稿由用户明确导入;不自动认领其他本机数据。
|
- **数据:** 以userId建立账户缓存命名空间;旧草稿键清理,不提供导入;未提交内容只在当前流程内存中传递。
|
||||||
- **接口:** GET /home、GET /dictionaries,以及所有接口共用协议。 具体方法和字段见第5章;均按计划新增/改造处理。
|
- **接口:** GET /home、GET /dictionaries,以及所有接口共用协议。 具体方法和字段见第5章;均按计划新增/改造处理。
|
||||||
- **异常与边界:** 生产缺API地址直接报配置错误;禁止静默降级成demo。断网不删除服务端事实。
|
- **异常与边界:** 生产缺API地址直接报配置错误;禁止静默降级成demo。断网不删除服务端事实。
|
||||||
|
|
||||||
@@ -187,14 +278,14 @@
|
|||||||
| --- | --- | --- | --- | --- | ---: |
|
| --- | --- | --- | --- | --- | ---: |
|
||||||
| BASE-001 | 接口契约与字段字典:定义版本路由、响应、错误、分页、ID、金额、时间及接口状态;确认前后端字段映射。 | P0 | 无 | 接口样例可同时被 PHP 和 UTS 解析;区分占位接口与后端现有实现。 | 0.5–1 |
|
| BASE-001 | 接口契约与字段字典:定义版本路由、响应、错误、分页、ID、金额、时间及接口状态;确认前后端字段映射。 | P0 | 无 | 接口样例可同时被 PHP 和 UTS 解析;区分占位接口与后端现有实现。 | 0.5–1 |
|
||||||
| BASE-002 | 统一请求与认证适配:统一共享/账户请求;后端识别 Bearer;解析业务失败和401;限定 CORS;记录 requestId。 | P0 | BASE-001 | HTTP 200 中业务失败不判成功;过期 Token 清会话;普通业务按所有者鉴权。 | 1–1.5 |
|
| BASE-002 | 统一请求与认证适配:统一共享/账户请求;后端识别 Bearer;解析业务失败和401;限定 CORS;记录 requestId。 | P0 | BASE-001 | HTTP 200 中业务失败不判成功;过期 Token 清会话;普通业务按所有者鉴权。 | 1–1.5 |
|
||||||
| BASE-003 | 环境与用户缓存隔离:开发/测试/生产配置;生产禁用 mock;会话和草稿等缓存按不可变 userId 隔离。 | P0 | BASE-002 | 切换账户无串数据;匿名旧草稿仅在用户明确选择导入后绑定;退出清理敏感缓存。 | 1–1.5 |
|
| BASE-003 | 环境与用户缓存隔离:开发/测试/生产配置;生产禁用 mock;会话等账户缓存按不可变 userId 隔离;未提交表单只存当前流程内存。 | P0 | BASE-002 | 切换账户无串数据;旧草稿键清理且不恢复;退出清理敏感缓存和临时填写内容。 | 1–1.5 |
|
||||||
| BASE-004 | 首页和联调骨架:首页接入聚合数据、加载/空态/失败重试及登录回跳;建立接口冒烟入口。 | P1 | BASE-002、BASE-003 | 深链接与刷新正确恢复;首页无硬编码账户数据;真实未读数后续由 MSG 接入。 | 0.5–1 |
|
| BASE-004 | 首页和联调骨架:首页接入聚合数据、加载/空态/失败重试及登录回跳;建立接口冒烟入口。 | P1 | BASE-002、BASE-003 | 深链接与刷新正确恢复;首页无硬编码账户数据;真实未读数后续由 MSG 接入。 | 0.5–1 |
|
||||||
|
|
||||||
### 4.2 AUTH|账户与短信安全
|
### 4.2 AUTH|账户与短信安全
|
||||||
|
|
||||||
**目标:** 让账户身份、个人资料和安全操作形成真实服务端闭环。 **阶段:** M1。**剩余:** 5–8人日。
|
**目标:** 让账户身份、个人资料和安全操作形成真实服务端闭环。 **阶段:** M1。**剩余:** 5–8人日。
|
||||||
|
|
||||||
- **现状:** 密码登录、本人资料读取、退出和原密码改密已接Redis会话;短信注册/登录、换绑和资料更新仍待完成,本机资料仅作回显缓存。
|
- **现状:** 密码登录、本人资料读取、退出和原密码改密已接Redis会话;资料修改与头像上传已完成本地验证;短信注册/登录、换绑仍待完成,本机资料仅作回显缓存。
|
||||||
- **前端:** 接通短信/密码登录,读取profile;头像独立上传;安全操作成功后重新登录。
|
- **前端:** 接通短信/密码登录,读取profile;头像独立上传;安全操作成功后重新登录。
|
||||||
- **后端:** 验证码用途、用户和手机号绑定;换绑核验当前及新号码;修复旧免登录改绑声明;修改密码/手机号撤销全部会话。
|
- **后端:** 验证码用途、用户和手机号绑定;换绑核验当前及新号码;修复旧免登录改绑声明;修改密码/手机号撤销全部会话。
|
||||||
- **数据:** 复用fa_user/fa_sms;会话改用Redis;手机号冲突由唯一约束兜底;昵称不覆盖实名。
|
- **数据:** 复用fa_user/fa_sms;会话改用Redis;手机号冲突由唯一约束兜底;昵称不覆盖实名。
|
||||||
@@ -246,21 +337,21 @@
|
|||||||
|
|
||||||
### 4.5 ASSESS|评估与融资档案
|
### 4.5 ASSESS|评估与融资档案
|
||||||
|
|
||||||
**目标:** 个人与企业评估可云端续填、重复评估、可靠提交并查看历史。 **阶段:** M2。**剩余:** 5–8人日。
|
**目标:** 个人与企业评估正式提交后持久保存,可重复评估并查看只读历史;不提供草稿。 **阶段:** M2。**剩余:** 5–8人日。
|
||||||
|
|
||||||
- **现状:** 表单、金额规范化和内部生命周期已有;无草稿API和真实提交链路。
|
- **现状:** 无草稿;需求v2录入/确认/提交、v1/v2历史分页与详情已接通。分析任务和重试继续生成v1自填报告;准备状态和报告v2自填支线已实现;真实产品匹配待实施。
|
||||||
- **前端:** 草稿revision控制、字段映射、服务端校验错误定位;保留assessmentId在所有后续路由。
|
- **前端:** 字段映射、服务端校验错误定位;确认前只保留内存,正式提交返回assessmentId后贯穿所有后续路由。
|
||||||
- **后端:** 认证用户创建草稿,保存时条件更新;完整校验后调用生命周期服务;提交后不可修改,修改需求转新评估。
|
- **后端:** 认证用户正式提交后,同一事务创建评估和接受任务;完整校验、请求幂等;提交后不可修改,修改需求转新评估。
|
||||||
- **数据:** 使用fa_user_info和唯一评估会话;前端本机版本与服务端revision区分;taskId用于任务尝试。
|
- **数据:** 使用fa_user_info和唯一评估会话;不新增草稿版本字段;taskId用于任务尝试。
|
||||||
- **接口:** POST/GET /assessments、GET/PUT /assessments/:id、POST /assessments/:id/submit、GET /assessments/:id/status、POST /assessments/:id/retry。 具体方法和字段见第5章;均按计划新增/改造处理。
|
- **接口:** POST/GET /assessments、GET /assessments/:id、GET /assessments/:id/status、POST /assessments/:id/retry均已实现自填支线;具体版本与字段见第5章。附件和正式授权待接。
|
||||||
- **异常与边界:** 草稿版本冲突提示重新读取;重复提交返回同一任务;跨用户记录统一404;失败重试保持原输入。
|
- **异常与边界:** 未提交内容不恢复;重复正式提交返回同一接受回执;跨用户记录统一404;失败重试保持原输入。
|
||||||
|
|
||||||
| 任务编号 | 任务与交付范围 | 优先级 | 前置任务/外部条件 | 验收条件 | 人日 |
|
| 任务编号 | 任务与交付范围 | 优先级 | 前置任务/外部条件 | 验收条件 | 人日 |
|
||||||
| --- | --- | --- | --- | --- | ---: |
|
| --- | --- | --- | --- | --- | ---: |
|
||||||
| ASSESS-001 | 评估字段与增量模型:确定枚举、地区代码、经营月数、附件引用、授权快照和前后端完整字段映射。 | P0 | BASE-001 | 未知与0区分;个人/企业必填口径明确;新增字段列独立迁移,不重建结果表。 | 1–2 |
|
| ASSESS-001 | 评估字段与增量模型:统一现有字典并补融资需求四字段、inputVersion及增量列;用途字典/必填规则按专项契约确认,保留附件和授权映射。 | P0 | BASE-001、DEC-DEMAND | 现有与v2字段映射明确,元字符串/整数月/用途条件校验;历史新字段NULL,不新建评估表。 | 1–2 |
|
||||||
| ASSESS-002 | 云端草稿与档案:新建/保存/读取草稿、版本冲突、列表、复制既往资料新建评估及用户隔离。 | P0 | ASSESS-001、AUTH-002 | 同用户可有多次同类型评估;不同账号不能读写;历史输入不被编辑覆盖。 | 1–2 |
|
| ASSESS-002 | 正式评估记录与历史:本人历史分页/只读详情及用户隔离;补v1/v2需求回显与旧未采集提示,无草稿保存或续填。 | P0 | ASSESS-001、AUTH-002 | 同用户可多次评估;越权拒绝;历史v1不回填需求、不重算,v2显示冻结需求且不可编辑。 | 1–2 |
|
||||||
| ASSESS-003 | 提交与生命周期API:接入submit/owned/retry,服务端校验;持久保存每次提交/重试请求键、参数摘要与taskId,返回原接受响应。 | P0 | ASSESS-002、FILE-004 | 同requestId只提交一次;多次重试后回放旧键仍返回原taskId;旧任务不可回写,完成方法不对客户端开放。 | 1.5–2 |
|
| ASSESS-003 | 提交与生命周期API:保留提交201及重试202幂等;补inputVersion切换、新需求校验、输入字典冻结、同键全字段比较和旧请求重放兼容。 | P0 | ASSESS-002、FILE-004 | 同键同内容原回执,合法需求或版本变化409、非法格式422;最低新建版本2或用途停用不阻断原键回放;旧重试返回原taskId。 | 1.5–2 |
|
||||||
| ASSESS-004 | 评估全页面联调:评估/确认/分析/档案传递assessmentId;处理金额、退出续填、并发及历史跳转。 | P0 | ASSESS-003 | 同一评估ID贯穿流程;0.800001万元精确提交为8000.01元;处理中不显示假报告。 | 1.5–2 |
|
| ASSESS-004 | 评估全页面联调:评估/确认/分析/报告/档案贯穿assessmentId;新增资金需求分组和只读展示,保留金额、清理与并发回归。 | P0 | ASSESS-003 | 个人/企业需求均能提交及回显;12.345678万元=123456.78元;无草稿,历史不串账户,处理中无假报告。 | 1.5–2 |
|
||||||
|
|
||||||
### 4.6 DATA|外部征信或数据查询
|
### 4.6 DATA|外部征信或数据查询
|
||||||
|
|
||||||
@@ -293,16 +384,16 @@
|
|||||||
|
|
||||||
| 任务编号 | 任务与交付范围 | 优先级 | 前置任务/外部条件 | 验收条件 | 人日 |
|
| 任务编号 | 任务与交付范围 | 优先级 | 前置任务/外部条件 | 验收条件 | 人日 |
|
||||||
| --- | --- | --- | --- | --- | ---: |
|
| --- | --- | --- | --- | --- | ---: |
|
||||||
| PRODUCT-001 | 规则字典与评分口径:统一硬准入、未知值处理、补充规则白名单、匹配评分和理由;准备人工确认样例。 | P0 | ASSESS-001、DEC-MATCH | 版本化规则与样例可审阅;地区/经营时间可确定比较;规则不执行任意表达式。 | 1.5–2 |
|
| PRODUCT-001 | 规则字典与评分口径:确定准入三态、来源要求、单产品需求覆盖、用途规则、证据结构和版本化排序;评分未发布时null。 | P0 | ASSESS-001、DEC-MATCH | 发布规则与人工样例可审阅;非法字段/操作符阻止发布;fail优先于unknown,匹配指标不作为审批概率。 | 1.5–2 |
|
||||||
| PRODUCT-002 | 准入与排序执行器:筛选上架/有效期/地区/对象及硬条件;执行补充规则、排序并输出可解释理由。 | P0 | PRODUCT-001 | 下架/过期/硬条件不符不推荐;缺失条件不当已满足;同数据同版本结果稳定。 | 2–3 |
|
| PRODUCT-002 | 准入与排序执行器:执行上架/有效期/对象/地区及需求硬规则;保存pass/fail/unknown证据,生成eligibility和稳定rank。 | P0 | PRODUCT-001 | 同输入/快照结果稳定;未知不当满足,配置错误不归咎用户;需求不拆额、不改期;未运行与零候选区分。 | 2–3 |
|
||||||
| PRODUCT-003 | 产品后台与公共查询:补齐规则编辑校验、发布预检、产品列表/详情与范围字段;录入真实测试产品。 | P0 | PRODUCT-001、BASE-002、DEC-PRODUCT | 上架完整校验;版本递增;公共详情无用户分数;真实产品有运营确认依据。 | 1.5–2 |
|
| PRODUCT-003 | 产品后台与公共查询:补规则条件编辑、来源/用途依据、发布预检、版本与公共详情;录入真实产品,缺依据阻止发布。 | P0 | PRODUCT-001、BASE-002、DEC-PRODUCT | 产品用途/地区/额度/期限有真实依据;规则可配置并启停,版本变化留依据;公共详情无个人分数。 | 1.5–2 |
|
||||||
| PRODUCT-004 | 匹配页面与快照:独立MatchResult类型、额度/期限/利率范围、匹配列表排序、产品分享与申请准备。 | P0 | PRODUCT-002、PRODUCT-003、ASSESS-004 | 按assessmentId显示;参数历史快照稳定;当前不可用产品清晰提示;无匹配有说明。 | 1–2 |
|
| PRODUCT-004 | 匹配页面与快照:按assessmentId展示三态分组、最多三个候选及分页;结果详情展示依据/注意项/AI解释,保留产品快照与申请准备。 | P0 | PRODUCT-002、PRODUCT-003、ASSESS-004 | 每条解释可追溯且不凑三正三反;当前产品变化不改历史;跨账户拒绝;unavailable/零候选/failed不混淆。 | 1–2 |
|
||||||
|
|
||||||
### 4.8 REPORT|AI任务、报告与历史
|
### 4.8 REPORT|AI任务、报告与历史
|
||||||
|
|
||||||
**目标:** 把真实输入、查询依据和产品匹配生成可追溯的DeepSeek报告。 **阶段:** M2。**剩余:** 8–12人日。
|
**目标:** 把真实输入、查询依据和产品匹配生成可追溯的DeepSeek报告。 **阶段:** M2。**剩余:** 8–12人日。
|
||||||
|
|
||||||
- **现状:** report_data/match_result及complete服务已有;队列配置存在但没有业务Job,页面为计时和静态分数。
|
- **现状:** v1自填报告已真实模型联调;v2需求、完整度、准备状态和冻结策略已完成假模型与页面联调。v2真实模型语义、产品证据/解释及外部渠道仍待验收。
|
||||||
- **前端:** 轮询真实状态、刷新恢复、失败重试;渲染数据驱动维度、来源时间和版本;查看历史。
|
- **前端:** 轮询真实状态、刷新恢复、失败重试;渲染数据驱动维度、来源时间和版本;查看历史。
|
||||||
- **后端:** DeepSeek适配、结构校验、任务抢占/超时/漏投补偿、输入最小化、用量记录;在事务中同时落报告、匹配和成功状态。
|
- **后端:** DeepSeek适配、结构校验、任务抢占/超时/漏投补偿、输入最小化、用量记录;在事务中同时落报告、匹配和成功状态。
|
||||||
- **数据:** result_version/rule_version/model与产品快照分别记录;模型输出不能覆盖金额、硬准入或数据来源。
|
- **数据:** result_version/rule_version/model与产品快照分别记录;模型输出不能覆盖金额、硬准入或数据来源。
|
||||||
@@ -312,9 +403,9 @@
|
|||||||
| 任务编号 | 任务与交付范围 | 优先级 | 前置任务/外部条件 | 验收条件 | 人日 |
|
| 任务编号 | 任务与交付范围 | 优先级 | 前置任务/外部条件 | 验收条件 | 人日 |
|
||||||
| --- | --- | --- | --- | --- | ---: |
|
| --- | --- | --- | --- | --- | ---: |
|
||||||
| REPORT-001 | DeepSeek适配与配额:配置模型标识、服务端密钥读取、超时、结构化输出校验、配额预留和消耗记录。 | P0 | BASE-002、EXT-AI、DEC-AI | 模型可配置;密钥不下发;失败与使用量可追踪;数值事实不被模型改写。 | 1.5–2 |
|
| REPORT-001 | DeepSeek适配与配额:配置模型标识、服务端密钥读取、超时、结构化输出校验、配额预留和消耗记录。 | P0 | BASE-002、EXT-AI、DEC-AI | 模型可配置;密钥不下发;失败与使用量可追踪;数值事实不被模型改写。 | 1.5–2 |
|
||||||
| REPORT-002 | 异步任务与补偿:think-queue/Redis业务Job、提交后派发、漏投扫描、租约/超时、重试和任务隔离。 | P0 | ASSESS-003、REPORT-001 | 派发失败/worker重启可恢复;旧taskId不能落结果;重复消费不重复完成。 | 2.5–4 |
|
| REPORT-002 | 异步任务与补偿:保留Redis派发、补投、租约和taskId隔离;沿用提交时input_snapshot,一次性补入execution_snapshot并在重试时复用,支持跨部署v1/v2任务。 | P0 | ASSESS-003、REPORT-001 | 漏投/中断可恢复或明确失败;旧任务不可回写;输入/目标结果/策略/产品快照重试不隐式变化。 | 2.5–4 |
|
||||||
| REPORT-003 | 报告生成与持久化:冻结已授权查询结果和产品匹配快照,调用AI解释,原子写report_data/match_result及状态。 | P0 | REPORT-002、DATA-004、PRODUCT-004、DEC-MATCH | 结构无效不得记成功;评分/证据来源明确;产品改动不改历史;部分资料缺失有标注。 | 2–3 |
|
| REPORT-003 | 报告生成与持久化:保留v1自填报告;补v2需求、完整度/准备等级、规则证据及定性AI区块;版本快照和正式结果原子落库。 | P0 | REPORT-002、DATA-004、PRODUCT-004、DEC-MATCH、DEC-READINESS | 完整度按发布字段集复算,等级有依据且非审批;null rank/score按状态验证;格式错误不成功,产品/规则更新不改旧报告。 | 2–3 |
|
||||||
| REPORT-004 | 分析报告历史联调:状态轮询、刷新恢复、失败重试、真实雷达维度、历史详情和模型版本展示。 | P0 | REPORT-003、ASSESS-004 | 个人/企业各完成一条真实闭环;分析失败不会跳成功报告;再次评估生成独立记录。 | 2–3 |
|
| REPORT-004 | 分析报告历史联调:保留轮询/重试和v1历史;接v2需求、完整度、准备等级、三态匹配及证据解释,不显示无规则分数/雷达。 | P0 | REPORT-003、ASSESS-004 | 个人/企业真实链路按版本展示;未发布无分或等级,100%不等于授信;旧报告不重算;新复评新assessmentId。 | 2–3 |
|
||||||
|
|
||||||
### 4.9 CHAT|AI助手
|
### 4.9 CHAT|AI助手
|
||||||
|
|
||||||
@@ -447,15 +538,19 @@
|
|||||||
|
|
||||||
### 5.1 实现状态与通用规则
|
### 5.1 实现状态与通用规则
|
||||||
|
|
||||||
**本章为目标契约;已实现子集为密码登录、本人资料读取、退出、原密码改密,短信仅实现503未配置响应,其余仍待开发。** 已有FastAdmin通用控制器,现新增api/controller/v1/Base.php和Session.php;未列为已实现的前端同名方法仍是接口占位。AUTH可复用通用服务实现,其他模块需新增业务控制器及路由。旧接口保留兼容,新H5统一走版本前缀;管理后台保持原路由。
|
**本章区分已实现与目标契约;已实现密码登录、本人资料读取/更新、头像上传、退出、原密码改密、正式评估提交/历史/详情/状态。短信仅实现503未配置响应,其余仍待开发。** 已有FastAdmin通用控制器,现使用api/controller/v1/Base.php、Session.php和Assessments.php;未列为已实现的前端同名方法仍是接口占位。AUTH可复用通用服务实现,其他模块需新增业务控制器及路由。旧接口保留兼容,新H5统一走版本前缀;管理后台保持原路由。
|
||||||
|
|
||||||
| 方法与路径(省略/api/v1) | 当前实现状态 | 本轮边界 |
|
| 方法与路径(省略/api/v1) | 当前实现状态 | 本轮边界 |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| POST /auth/password-login | 已实现并本地联调 | 既有手机号账户、密码验证、旧散列升级、Redis会话;沿用账户失败次数限制,IP级频控待补 |
|
| POST /auth/password-login | 已实现并本地联调 | 既有手机号账户、密码验证、旧散列升级、Redis会话;沿用账户失败次数限制,IP级频控待补 |
|
||||||
| GET /account/profile | 已实现并HTTP验证 | 返回userId/nickname/avatarUrl/phoneMasked;完整security字段与资料修改待补 |
|
| GET /account/profile | 已实现并HTTP验证 | 返回userId/nickname/avatarUrl/phoneMasked;完整security字段待补 |
|
||||||
| POST /auth/logout | 已实现并本地联调 | 撤销当前Token;成功或已401清本机会话,网络失败保留以便重试撤销 |
|
| POST /auth/logout | 已实现并本地联调 | 撤销当前Token;成功或已401清本机会话,网络失败保留以便重试撤销 |
|
||||||
| PUT /account/password | 原密码方式已实现并验证 | verification=password;成功撤销全部会话;sms方式返回503 |
|
| PUT /account/password | 原密码方式已实现并验证 | verification=password;成功撤销全部会话;sms方式返回503 |
|
||||||
| POST /auth/sms-code、/auth/sms-login | 仅未配置响应 | HTTP 503、data.errorCode=SMS_NOT_CONFIGURED,无短信发送或注册 |
|
| POST /auth/sms-code、/auth/sms-login | 仅未配置响应 | HTTP 503、data.errorCode=SMS_NOT_CONFIGURED,无短信发送或注册 |
|
||||||
|
| PUT /account/profile、POST /account/avatar | 已实现并本地验证 | 昵称持久化、头像校验上传与附件归属;avatarId=null恢复默认 |
|
||||||
|
| POST /assessments、GET /assessments、GET /assessments/:id、GET /assessments/:id/status | 已实现并本地联调 | 已支持v2需求提交及v1/v2只读历史;自填报告/任务/retry已接,产品匹配待开发 |
|
||||||
|
| GET /dictionaries | 已实现并本地验证 | 公开用途字典及输入版本,no-store,地区等字典仍待补 |
|
||||||
|
| POST /assessments/:id/retry、GET /assessments/:id/report | 已实现并本地联调 | 幂等重试及resultVersion=1/2自填报告;真实产品/外部数据支线待开发 |
|
||||||
| 其余本章接口 | 计划新增 | 当前未配置的v1路由返回JSON 404,不判业务成功 |
|
| 其余本章接口 | 计划新增 | 当前未配置的v1路由返回JSON 404,不判业务成功 |
|
||||||
|
|
||||||
| 项目 | 约定 |
|
| 项目 | 约定 |
|
||||||
@@ -468,7 +563,7 @@
|
|||||||
| 失败包装 | code非1;data包含errorCode、可选fieldErrors、requestId;前端不把HTTP 200业务失败当成功 |
|
| 失败包装 | code非1;data包含errorCode、可选fieldErrors、requestId;前端不把HTTP 200业务失败当成功 |
|
||||||
| HTTP错误 | 400格式错误,401登录失效,403操作无权限,404记录不存在/不属于本人,409状态/版本冲突,422字段错误,429频控,503依赖不可用 |
|
| HTTP错误 | 400格式错误,401登录失效,403操作无权限,404记录不存在/不属于本人,409状态/版本冲突,422字段错误,429频控,503依赖不可用 |
|
||||||
| 错误实现 | FastAdmin默认状态推导不足时显式传statuscode;兼容旧接口错误体但新接口使用稳定errorCode |
|
| 错误实现 | FastAdmin默认状态推导不足时显式传statuscode;兼容旧接口错误体但新接口使用稳定errorCode |
|
||||||
| 分页 | page默认1,pageSize默认20、最大50;items/total/page/pageSize;稳定排序含唯一ID |
|
| 分页 | 默认目标为page/pageSize;已实现评估历史使用cursor/limit和items/hasMore/nextCursor,默认20最大50,以第5.4节为准 |
|
||||||
| 列表范围 | 公开列表只取已发布有效内容;用户列表查询始终带user_id |
|
| 列表范围 | 公开列表只取已发布有效内容;用户列表查询始终带user_id |
|
||||||
| 金额与比例 | 元字符串两位小数;利率为百分数;月数整数;展示时通过统一金额服务换算 |
|
| 金额与比例 | 元字符串两位小数;利率为百分数;月数整数;展示时通过统一金额服务换算 |
|
||||||
| 幂等 | 表格注明业务键;同键不同业务参数返回409;成功重复调用返回同一业务结果;前端显式重试复用原键 |
|
| 幂等 | 表格注明业务键;同键不同业务参数返回409;成功重复调用返回同一业务结果;前端显式重试复用原键 |
|
||||||
@@ -515,14 +610,16 @@
|
|||||||
| --- | --- |
|
| --- | --- |
|
||||||
| Session | token:string、userId:string、expiresAt:ISO时间、profile:Profile;令牌只在认证响应出现 |
|
| Session | token:string、userId:string、expiresAt:ISO时间、profile:Profile;令牌只在认证响应出现 |
|
||||||
| Profile | userId、nickname:string(2–20字符)、avatarUrl:string或null、phoneMasked、security状态;手机号完整值仅在必要且认证的操作中使用 |
|
| Profile | userId、nickname:string(2–20字符)、avatarUrl:string或null、phoneMasked、security状态;手机号完整值仅在必要且认证的操作中使用 |
|
||||||
| AssessmentDraft | assessmentId、type:personal/enterprise、revision:int、step:0–4、moneyUnit:yuan、第3.3节表单字段、documents数组、consentIds数组;未填字段null |
|
| AssessmentInput | 已有v1字段保留;已实现inputVersion=2及需求四字段,详见专项契约第2节;新建最低版本2,旧v1仅原键重放;新建与历史重放版本策略见第7节 |
|
||||||
| AssessmentStatus | assessmentId、taskId、status:draft/processing/succeeded/failed、stage:string或null、submittedAt/completedAt、errorCode、可显示错误、canRetry |
|
| AssessmentStatus | 已实现assessmentId、type、status:processing/succeeded/failed、statusLabel、message、submittedAt、city、errorCode;现已返回taskId:string、stage:queued/running/completed/failed、attemptCount:int、canRetry:boolean;completedAt暂不返回 |
|
||||||
| Document | attachmentId、type、name、mime、sizeBytes、status、createdAt;无永久公开证件URL |
|
| Document | attachmentId、type、name、mime、sizeBytes、status、createdAt;无永久公开证件URL |
|
||||||
| Consent | consentId、scope、agreementCode、agreementVersion、grantedAt、revokedAt;第三方授权完成状态独立于本地意向 |
|
| Consent | consentId、scope、agreementCode、agreementVersion、grantedAt、revokedAt;第三方授权完成状态独立于本地意向 |
|
||||||
| Product | id、name、bankName、objects数组、amountMinYuan/amountMaxYuan:string、termMinMonths/termMaxMonths:int、annualRateMin/annualRateMax:string、regions、version、说明字段、当前可用状态 |
|
| Product | id、name、bankName、objects数组、amountMinYuan/amountMaxYuan:string、termMinMonths/termMaxMonths:int、annualRateMin/annualRateMax:string、regions、version、说明字段、当前可用状态 |
|
||||||
| MatchResult | productId、rank:int、score:number、reasons:string[]、productSnapshot:ProductSnapshot;score范围与计算由DEC-MATCH定稿 |
|
| MatchResult(计划v2) | productId/productVersion、eligibility、rank:int或null、score:number或null、positiveReasons/blockingReasons/cautions、evidence、aiExplanation、productSnapshot;当前complete的rank/score非空校验需升级,详见专项契约第5节 |
|
||||||
| ProductSnapshot | 产品ID/名称/版本、额度元字符串、期限月、年化百分数及必要准入说明;不可用当前产品字段覆盖 |
|
| ProductSnapshot | 产品ID/名称/版本、额度元字符串、期限月、年化百分数及必要准入说明;不可用当前产品字段覆盖 |
|
||||||
| Report | resultVersion:int、ruleVersion:string、assessmentId、generatedAt、modelCode、overallScore:number或null、dimensions数组、summary、suggestions、dataGaps、sourceRefs |
|
| Report(已有自填版本) | resultVersion:1、reportType:self_reported_analysis、assessmentId:string、generatedAt:ISO、modelCode:string、overallScore:null、facts数组、summary:string、observations:[{factId,text}]、suggestions:string[]、dataGaps:string[]、matchingStatus:unavailable、matchingMessage、disclaimer;有规则评分及产品快照版本仍计划新增 |
|
||||||
|
| Report(已有v2自填支线) | resultVersion:2、reportType:financing_needs_analysis、inputVersion、demand、facts、completeness、readiness、consistencyIssues、matching、summary/strengths/improvements/riskWarnings、policyVersions、systemWarnings、disclaimer;见专项契约第6节,matching当前只支持unavailable |
|
||||||
|
| Completeness / Readiness | 服务器计算,均含status/policyVersion;未发布规则时计数/percent或code为null,不由模型生成;详见专项契约第3—4节 |
|
||||||
| ReportDimension | code、label、score:number或null、explanation、evidenceRefs;数据不足明确null,区分用户自填和渠道核验 |
|
| ReportDimension | code、label、score:number或null、explanation、evidenceRefs;数据不足明确null,区分用户自填和渠道核验 |
|
||||||
| Chat | chatId、title、scene:assistant、contextAssessmentId:string或null、updatedAt |
|
| Chat | chatId、title、scene:assistant、contextAssessmentId:string或null、updatedAt |
|
||||||
| ChatMessage | messageId、chatId、sequence:int、role、status、text、requestId、errorCode、createdAt;模型用量按权限返回摘要 |
|
| ChatMessage | messageId、chatId、sequence:int、role、status、text、requestId、errorCode、createdAt;模型用量按权限返回摘要 |
|
||||||
@@ -531,7 +628,7 @@
|
|||||||
| PointsLedger | id、delta:int、before:int、after:int、businessType、businessId、memo、createdAt |
|
| PointsLedger | id、delta:int、before:int、after:int、businessType、businessId、memo、createdAt |
|
||||||
| Feedback | id、content、attachmentIds、status、reply、createdAt、handledAt;用户端不暴露无关管理信息 |
|
| Feedback | id、content、attachmentIds、status、reply、createdAt、handledAt;用户端不暴露无关管理信息 |
|
||||||
|
|
||||||
数据库JSON采用已有snake_case字段时,由DTO统一转换为camelCase;文档中的API对象不是直接暴露数据库行。例如持久match_result仍含product_id/rank/score/reasons/product_snapshot,API转换为productId/productSnapshot。
|
数据库JSON采用已有snake_case字段时,由DTO统一转换为camelCase;文档中的API对象不是直接暴露数据库行。当前match_result结构使用product_id/rank/score/reasons/product_snapshot;计划v2补eligibility、positive_reasons/blocking_reasons/cautions及evidence等字段,具体映射见专项契约第5.3节,不沿用单一reasons表达所有类型的解释。
|
||||||
|
|
||||||
### 5.3 基础、账户与第三方登录接口
|
### 5.3 基础、账户与第三方登录接口
|
||||||
|
|
||||||
@@ -540,7 +637,7 @@
|
|||||||
| 方法与路径 | 权限 | 入参 | data响应 | 错误/幂等要点 |
|
| 方法与路径 | 权限 | 入参 | data响应 | 错误/幂等要点 |
|
||||||
| --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- |
|
||||||
| GET /home | 公开,登录后附本人摘要 | 无 | banners、recommendations、assessmentSummary、unreadCount | 未登录的个人字段null;内部聚合失败不伪造数据 |
|
| GET /home | 公开,登录后附本人摘要 | 无 | banners、recommendations、assessmentSummary、unreadCount | 未登录的个人字段null;内部聚合失败不伪造数据 |
|
||||||
| GET /dictionaries | 公开 | version可选 | 字典版本、地区、枚举和表单选项 | 支持版本缓存;不能读取已删除fa_area |
|
| GET /dictionaries | 公开 | 无 | assessmentInputVersions:int[]、minNewAssessmentInputVersion:int、financingPurposes:{version,items} | 已实现;no-store;用途项type/code/label:string、requiresDetail/enabled:boolean;地区等字典待补,不读取已删除fa_area |
|
||||||
| POST /auth/sms-code | 公开;绑定用途需有效bindingTicket | phone:string、purpose:login/bind-phone、bindingTicket仅绑定时必填、requestId | challengeId、retryAfterSeconds | 频控;同requestId不重复发送;绑定挑战关联bindingTicket与目标手机号,登录验证码不可用于绑定 |
|
| POST /auth/sms-code | 公开;绑定用途需有效bindingTicket | phone:string、purpose:login/bind-phone、bindingTicket仅绑定时必填、requestId | challengeId、retryAfterSeconds | 频控;同requestId不重复发送;绑定挑战关联bindingTicket与目标手机号,登录验证码不可用于绑定 |
|
||||||
| POST /auth/sms-login | 公开 | phone、challengeId、code:string | Session | 验证码核销;手机号唯一;错误不暴露账户细节 |
|
| POST /auth/sms-login | 公开 | phone、challengeId、code:string | Session | 验证码核销;手机号唯一;错误不暴露账户细节 |
|
||||||
| POST /auth/password-login | 公开 | phone、password:string | Session | 限流;旧密码登录遵循原验证,成功后升级散列 |
|
| POST /auth/password-login | 公开 | phone、password:string | Session | 限流;旧密码登录遵循原验证,成功后升级散列 |
|
||||||
@@ -569,25 +666,37 @@
|
|||||||
| GET /consents | 登录 | scope可选 | Consent列表 | 仅当前用户 |
|
| GET /consents | 登录 | scope可选 | Consent列表 | 仅当前用户 |
|
||||||
| POST /consents | 登录 | scope、agreementCode、agreementVersion、requestId | Consent | 服务端记录时间;协议版本与展示一致;业务键幂等 |
|
| POST /consents | 登录 | scope、agreementCode、agreementVersion、requestId | Consent | 服务端记录时间;协议版本与展示一致;业务键幂等 |
|
||||||
| POST /consents/:id/revoke | 所有者 | requestId | Consent | 重复撤销返回既有时间;停止新查询,保留历史证据 |
|
| POST /consents/:id/revoke | 所有者 | requestId | Consent | 重复撤销返回既有时间;停止新查询,保留历史证据 |
|
||||||
| POST /assessments | 登录 | type、clientDraftId、sourceAssessmentId可选 | AssessmentDraft | 用户+clientDraftId唯一;仅可复制本人历史,复制时不继承旧授权 |
|
| POST /assessments | 登录 | 当前v2:type、requestId、form、documents:[]、consentIds:[]、moneyUnit:yuan;必填inputVersion=2及form.requestedAmountYuan/requestedTermMonths/purposeCode/purposeDetail | assessmentId、submissionStatus:accepted | v1原键回放与v2新建均HTTP201;v2已按专项契约校验元字符串/整数月/用途字典并冻结输入依据;同键同规范化内容同回执,异内容409,非法格式422;仅确认资料持久保存,不等于已派发分析任务 |
|
||||||
| GET /assessments | 登录 | type/status可选、page/pageSize | 草稿和历史分页 | 只本人且未软删除;按create_time/id稳定倒序 |
|
| GET /assessments | 登录 | type/status可选、cursor:string可选、limit:string默认20上限50 | items、hasMore:boolean、nextCursor:string或null | 已实现;只本人且未软删除、非draft;id倒序且后页id小于cursor,避免新增记录挤动页码 |
|
||||||
| GET /assessments/:id | 所有者 | 无 | AssessmentDraft及状态 | 非draft为只读;敏感字段按用途返回 |
|
| GET /assessments/:id | 所有者 | 无 | AssessmentStatus + moneyUnit:yuan + form;已返回inputVersion、四个需求字段及冻结purposeLabel(金额元字符串) | 已实现v1/v2只读;旧v1需求字段为null并标注未采集,v2已提交需求不可编辑;手机号掩码、身份证仅已提供标记,无密文/完整号码;越权与不存在均404 |
|
||||||
| PUT /assessments/:id | 所有者 | revision、step、form、documents、consentIds、moneyUnit:yuan | 更新后的AssessmentDraft | 仅draft;revision条件更新,冲突409;逐字段白名单 |
|
| GET /assessments/:id/status | 所有者 | 无 | AssessmentStatus | 已实现;仅processing时每5秒轮询,离开停止;不自动跳转原型报告 |
|
||||||
| POST /assessments/:id/submit | 所有者 | requestId、revision | assessmentId、taskId、status、stage | 全量校验后冻结;同键同结果;返回HTTP202 |
|
| POST /assessments/:id/retry | 所有者 | requestId:string,8–64位字母/数字/下划线/短横线 | assessmentId:string、taskId:string、status:processing、stage:queued | 已实现HTTP202;仅failed创建新尝试;同键始终回放原尝试;不同用户404、状态/次数上限409、日额度429、配置未开放503;不覆盖输入 |
|
||||||
| GET /assessments/:id/status | 所有者 | 无 | AssessmentStatus | 前端依据状态,不将动画作为进度事实 |
|
|
||||||
| POST /assessments/:id/retry | 所有者 | requestId | assessmentId、taskId、status | 仅failed可创建新尝试;同重试键返回同一尝试;不覆盖输入 |
|
|
||||||
| GET /assessments/:id/data-queries | 所有者 | 无 | 查询列表及来源摘要 | 原始敏感结果不直接下发 |
|
| GET /assessments/:id/data-queries | 所有者 | 无 | 查询列表及来源摘要 | 原始敏感结果不直接下发 |
|
||||||
| POST /assessments/:id/data-queries | 所有者 | scope、consentId、requestId | queryId、status | 服务端按状态与渠道限制发起;有效授权+业务键;HTTP202 |
|
| POST /assessments/:id/data-queries | 所有者 | scope、consentId、requestId | queryId、status | 服务端按状态与渠道限制发起;有效授权+业务键;HTTP202 |
|
||||||
| GET /data-queries/:id | 所有者 | 无 | queryId、status、source、sourceAt、errorCode、可显示摘要 | 费用/权限错误有明确状态;不返回渠道凭据 |
|
| GET /data-queries/:id | 所有者 | 无 | queryId、status、source、sourceAt、errorCode、可显示摘要 | 费用/权限错误有明确状态;不返回渠道凭据 |
|
||||||
| POST /callbacks/data/:provider | 渠道验签 | 供应商原始格式 | 供应商要求的ACK | 原始体验签、防重放、单号映射、幂等;不适用用户Bearer |
|
| POST /callbacks/data/:provider | 渠道验签 | 供应商原始格式 | 供应商要求的ACK | 原始体验签、防重放、单号映射、幂等;不适用用户Bearer |
|
||||||
| GET /assessments/:id/report | 所有者 | 无 | Report | 未成功返回409及状态;不生成临时假报告 |
|
| GET /assessments/:id/report | 所有者 | 无 | Report(v1/v2自填版本) | 已实现;未成功409 REPORT_NOT_READY,不存在/越权404,旧结构409 REPORT_FORMAT_UNAVAILABLE;Cache-Control:no-store |
|
||||||
| GET /assessments/:id/matches | 所有者 | 排序选项可选 | resultVersion、items:MatchResult[] | 只读取已完成快照;排序不修改历史rank |
|
| GET /assessments/:id/matches | 所有者 | eligibility默认eligible、limit默认3上限50、cursor可空 | assessmentId、resultVersion、matching、items、hasMore、nextCursor | 计划新增,三态分组、冻结快照与游标绑定,见专项契约第5.3节;当前尚未实现 |
|
||||||
|
| GET /assessments/:id/matches/:productId | 所有者 | 无 | 该次匹配结果、理由、证据和快照 | 计划新增;只读本次已评估项;越权/快照无该产品404;不从当前公共产品拼历史 |
|
||||||
| GET /products | 公开 | type/regionCode可选、page/pageSize | Product分页 | 仅当前有效上架产品;不返回用户分数 |
|
| GET /products | 公开 | type/regionCode可选、page/pageSize | Product分页 | 仅当前有效上架产品;不返回用户分数 |
|
||||||
| GET /products/:id | 公开 | 无 | Product | 下架/过期返回不可用语义;历史快照从本人匹配接口读取 |
|
| GET /products/:id | 公开 | 无 | Product | 下架/过期返回不可用语义;历史快照从本人匹配接口读取 |
|
||||||
|
|
||||||
评估主流程由服务端任务编排:提交后检查授权 → 获取选定的外部结果 → 冻结查询引用 → 规则匹配 → DeepSeek解释 → 原子保存正式结果。未选择可选外部授权时使用自填资料并标记来源;已选择查询但失败时是否允许降级到自填,需要DEC-QUERY形成明确规则。该规则未定稿前,相应错误按失败处理,不静默忽略。
|
当前已实现自填分析支线(提交→数据库queued→Redis→DeepSeek→报告);以下授权查询和产品规则主流程仍待实现:提交后检查授权 → 获取选定的外部结果 → 冻结查询引用 → 规则匹配 → DeepSeek解释 → 原子保存正式结果。未选择可选外部授权时使用自填资料并标记来源;已选择查询但失败时是否允许降级到自填,需要DEC-QUERY形成明确规则。该规则未定稿前,相应错误按失败处理,不静默忽略。
|
||||||
|
|
||||||
分析页建议2秒起轮询,逐步退避到5秒,离开页面停止前端轮询但不取消服务端任务;重新进入按assessmentId恢复。供应商超时和任务租约由配置控制并在REPORT-002用实际接口时限确定,不按前端10秒请求超时推断AI执行失败。
|
分析页当前只对processing每5秒轮询,离开页面停止前端轮询但不取消服务端任务;重新进入按assessmentId恢复。供应商总超时90秒、连接超时10秒,执行租约至少180秒、Redis reserved到期240秒,不按前端10秒请求超时推断AI执行失败。
|
||||||
|
|
||||||
|
|
||||||
|
#### 当前v2正式提交示例(所有路径前缀/api/v1,Bearer必填)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"inputVersion":2,"type":"enterprise","requestId":"submission_example_001","moneyUnit":"yuan","form":{"name":"示例联系人","phone":"19900000001","city":"深圳市","cityCode":"440300","enterpriseName":"示例企业","businessCode":"91440300123456789A","annualRevenue":"1201234.56","operatingMonths":18,"creditLoanCount":0,"totalDebtBalance":null,"requestedAmountYuan":"123456.78","requestedTermMonths":12,"purposeCode":"working_capital","purposeDetail":null},"documents":[],"consentIds":[]}
|
||||||
|
```
|
||||||
|
|
||||||
|
成功HTTP201:`{code:1,msg:"评估资料已保存",time:<Unix秒>,data:{assessmentId:"123",submissionStatus:"accepted"}}`。同键相同规范化内容重复返回相同data。GET /assessments?limit=20&cursor=123继续读取更小ID;data为`{items:[AssessmentStatus],hasMore:boolean,nextCursor:string|null}`。详情额外含form,金额始终是元字符串或null,客户端转万元。当前新建最低版本为2,示例已包含需求四字段;历史v1原键回放仍保留。
|
||||||
|
|
||||||
|
422 VALIDATION_ERROR表示字段、枚举、金额或分页错误;422 ASSESSMENT_ATTACHMENTS_UNAVAILABLE表示携带尚不支持的附件/授权;401 AUTH_REQUIRED表示未登录;404 ASSESSMENT_NOT_FOUND不区分越权与不存在;409 REQUEST_CONFLICT表示同键异内容或记录已移除。草稿路径及修改已提交资料未开放;retry已按上表实现;路由采用完整匹配。
|
||||||
|
|
||||||
|
当前枚举:maritalStatus=married/single/divorced/widowed;house/car=yes/no;socialSecurity/housingFund=six_months_plus/under_six_months/none,均允许null。计数字段creditLoanCount/creditCardCount/operatingMonths是0–9999的整数或null。个人必填name/idCard/phone/city/monthlyIncome>0,企业必填name/phone/city/enterpriseName/businessCode/annualRevenue>0;类型专属字段不得交叉混入。
|
||||||
|
|
||||||
### 5.5 助手、内容、消息、积分与客服接口
|
### 5.5 助手、内容、消息、积分与客服接口
|
||||||
|
|
||||||
@@ -624,7 +733,7 @@
|
|||||||
|
|
||||||
### 5.6 对现有占位接口的迁移
|
### 5.6 对现有占位接口的迁移
|
||||||
|
|
||||||
- 原POST /assessments“整包提交”拆成创建/更新草稿与submit;确认页必须保存返回assessmentId。
|
- POST /assessments仅负责正式提交,不创建可续填草稿;确认页必须保存返回assessmentId。
|
||||||
- 原GET /matches替换为GET /assessments/:id/matches,避免读取不明确的“最近结果”。
|
- 原GET /matches替换为GET /assessments/:id/matches,避免读取不明确的“最近结果”。
|
||||||
- 原POST /assistant/messages的message→reply同步接口,替换为会话消息异步接口,避免前端超时导致重复请求模型。
|
- 原POST /assistant/messages的message→reply同步接口,替换为会话消息异步接口,避免前端超时导致重复请求模型。
|
||||||
- 原FinanceProduct同时承载公共信息和score的结构,拆成Product与MatchResult;旧amount万元兼容仅在过渡适配器,正式API始终返回amountMinYuan/amountMaxYuan。
|
- 原FinanceProduct同时承载公共信息和score的结构,拆成Product与MatchResult;旧amount万元兼容仅在过渡适配器,正式API始终返回amountMinYuan/amountMaxYuan。
|
||||||
@@ -635,9 +744,9 @@
|
|||||||
|
|
||||||
## 6. 工时、里程碑与实施顺序
|
## 6. 工时、里程碑与实施顺序
|
||||||
|
|
||||||
### 6.1 剩余工程量
|
### 6.1 初始工程量基线(当前剩余量待重估)
|
||||||
|
|
||||||
| 模块 | 功能板块 | 阶段 | 剩余人日 |
|
| 模块 | 功能板块 | 阶段 | 基线人日 |
|
||||||
| --- | --- | --- | ---: |
|
| --- | --- | --- | ---: |
|
||||||
| BASE | 基础接口与环境 | M0 | 3–5 |
|
| BASE | 基础接口与环境 | M0 | 3–5 |
|
||||||
| AUTH | 账户与短信安全 | M1 | 5–8 |
|
| AUTH | 账户与短信安全 | M1 | 5–8 |
|
||||||
@@ -697,7 +806,7 @@ flowchart LR
|
|||||||
| EXT-ONE | 待选型 | H5号码认证产品、运营商/网络/浏览器矩阵、SDK与服务端校验、测试环境 | LOGIN-001、LOGIN-003 |
|
| EXT-ONE | 待选型 | H5号码认证产品、运营商/网络/浏览器矩阵、SDK与服务端校验、测试环境 | LOGIN-001、LOGIN-003 |
|
||||||
| EXT-STORAGE | 待选型 | 私有存储方案、访问控制、文件限制、备份与生命周期 | FILE-001 |
|
| EXT-STORAGE | 待选型 | 私有存储方案、访问控制、文件限制、备份与生命周期 | FILE-001 |
|
||||||
| EXT-DATA | 待选型 | 实际可开通数据产品、个人/企业接口、授权要求、样例、回调/查询和费用状态 | DATA-001 |
|
| EXT-DATA | 待选型 | 实际可开通数据产品、个人/企业接口、授权要求、样例、回调/查询和费用状态 | DATA-001 |
|
||||||
| EXT-AI | DeepSeek已选定,接入待核验 | 具体模型标识、测试权限、额度、超时/结构化输出支持及实际用量字段;实施时核对官方接口文档 | REPORT-001 |
|
| EXT-AI | 两个指定模型已真实可用,Flash自填分析通过 | JSON输出及tokens/耗时已核实;供应商余额、价格对账、商业预算仍待确认 | REPORT-001 |
|
||||||
| EXT-CS | 待选型 | H5接入、访客身份、坐席、在线/离线、历史与附件能力、工作时间 | SUPPORT-001、SUPPORT-003 |
|
| EXT-CS | 待选型 | H5接入、访客身份、坐席、在线/离线、历史与附件能力、工作时间 | SUPPORT-001、SUPPORT-003 |
|
||||||
| EXT-DEPLOY | 待确认 | 测试/生产资源、域名/HTTPS、Redis/worker守护、备份与监控 | RELEASE-003 |
|
| EXT-DEPLOY | 待确认 | 测试/生产资源、域名/HTTPS、Redis/worker守护、备份与监控 | RELEASE-003 |
|
||||||
|
|
||||||
@@ -705,14 +814,16 @@ flowchart LR
|
|||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| DEC-PRIVACY | 资料用途、可选项、正式协议正文与版本、保留期限、撤销后处置、第三方必要字段 | FILE-001、FILE-003:协议和数据生命周期清单 |
|
| DEC-PRIVACY | 资料用途、可选项、正式协议正文与版本、保留期限、撤销后处置、第三方必要字段 | FILE-001、FILE-003:协议和数据生命周期清单 |
|
||||||
| DEC-QUERY | 首发数据查询种类与个人/企业范围、拒绝/失败后的自填路径、重查与费用归属 | DATA-001:页面授权到数据产品映射及失败策略 |
|
| DEC-QUERY | 首发数据查询种类与个人/企业范围、拒绝/失败后的自填路径、重查与费用归属 | DATA-001:页面授权到数据产品映射及失败策略 |
|
||||||
| DEC-MATCH | 硬准入、未知处理、分数/等级/排序、报告维度、同分规则及人工样例 | PRODUCT-001、REPORT-003:版本化规则与金标准样例 |
|
| DEC-DEMAND | 已确认并于本地发布:用途字典、other说明、v2新建必填及最低版本2;见0.5 | ASSESS-001:发布的需求字典、校验样例与兼容计划;见专项契约第2/7节 |
|
||||||
|
| DEC-READINESS | 已按用户授权采用首版并发布开发版:等权字段集/条件项、准备状态及一致性规则;生产业务验收待完成 | REPORT-003:发布策略、策略版本与人工样例;未发布返回policy_unconfirmed |
|
||||||
|
| DEC-MATCH | 硬准入三态、来源要求、需求覆盖、用途依据、排序/可选匹配指标和解释证据;资料等级另由DEC-READINESS确认 | PRODUCT-001、REPORT-003:版本化规则与金标准样例 |
|
||||||
| DEC-PRODUCT | 首发真实产品及金额/期限/利率/地区/准入依据、有效期和运营责任人 | PRODUCT-003:可发布产品资料与运营确认记录 |
|
| DEC-PRODUCT | 首发真实产品及金额/期限/利率/地区/准入依据、有效期和运营责任人 | PRODUCT-003:可发布产品资料与运营确认记录 |
|
||||||
| DEC-AI | DeepSeek具体模型、单用户/评估额度、超时重试、成本预算与超额策略 | REPORT-001:配置及额度规则 |
|
| DEC-AI | 模型已确定;单用户/评估额度、超时重试、成本预算与超额策略待定 | REPORT-001:配置及额度规则 |
|
||||||
| DEC-POINT | 签到/资料/邀请奖励数额、次数、可信达成条件、权益/有效期 | POINT-001:活动规则及正反样例 |
|
| DEC-POINT | 签到/资料/邀请奖励数额、次数、可信达成条件、权益/有效期 | POINT-001:活动规则及正反样例 |
|
||||||
|
|
||||||
技术默认项:每类服务商首期一个;站内消息优先;课堂为图文;助手先异步完整回复;时间业务口径Asia/Shanghai;列表pageSize=20、上限50。业务分数、奖励数额、查询费用、模型名和数据保留期限不擅自定为上线规则。
|
技术默认项:每类服务商首期一个;站内消息优先;课堂为图文;助手先异步完整回复;时间业务口径Asia/Shanghai;列表pageSize=20、上限50。业务分数、奖励数额、查询费用、模型名和数据保留期限不擅自定为上线规则。
|
||||||
|
|
||||||
DeepSeek只选定服务商,不能据此宣布模型可用或已通过真实调用。一键登录也不能依据“原生App有SDK”推断H5全场景可用;若渠道不支持已承诺场景,先形成能力差异和影响,再记录用户的范围决定,不能静默删项。
|
DeepSeek已通过真实请求与自填报告验证,但不代表供应商余额充足或生产容量已验收。一键登录也不能依据“原生App有SDK”推断H5全场景可用;若渠道不支持已承诺场景,先形成能力差异和影响,再记录用户的范围决定,不能静默删项。
|
||||||
|
|
||||||
## 8. 验证、发布与文档维护
|
## 8. 验证、发布与文档维护
|
||||||
|
|
||||||
@@ -729,11 +840,14 @@ DeepSeek只选定服务商,不能据此宣布模型可用或已通过真实调
|
|||||||
| 场景 | 必须满足的结果 | 归属 |
|
| 场景 | 必须满足的结果 | 归属 |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| 金额换算 | 0.800001万元=8000.01元;最大值、超限、负数、空值、0和精度均校验;不重复换算 | ASSESS-004、PRODUCT-004 |
|
| 金额换算 | 0.800001万元=8000.01元;最大值、超限、负数、空值、0和精度均校验;不重复换算 | ASSESS-004、PRODUCT-004 |
|
||||||
| 身份与越权 | 用户A不能读取/修改B的草稿、证件、报告、会话、消息和反馈;改密/换绑后旧Token失效 | AUTH-004、RELEASE-001 |
|
| 身份与越权 | 用户A不能读取/修改B的评估、证件、报告、会话、消息和反馈;改密/换绑后旧Token失效 | AUTH-004、RELEASE-001 |
|
||||||
| 重复提交/历史重试回放 | 相同requestId只一条正式会话;多次重试后重放任一旧键仍返回其原taskId、不再次执行;同键参数冲突拒绝 | ASSESS-003 |
|
| 重复提交/历史重试回放 | 相同requestId只一条正式会话;多次重试后重放任一旧键仍返回其原taskId、不再次执行;同键参数冲突拒绝 | ASSESS-003 |
|
||||||
| 任务故障 | 派发失败、重复消费、worker退出、旧taskId回写均可恢复或安全拒绝 | REPORT-002 |
|
| 任务故障 | 派发失败、重复消费、worker退出、旧taskId回写均可恢复或安全拒绝 | REPORT-002 |
|
||||||
| 历史稳定 | 同类型多次评估分别保留;产品改价/下架不改变历史快照 | PRODUCT-004、REPORT-004 |
|
| 历史稳定 | 同类型多次评估分别保留;产品改价/下架不改变历史快照 | PRODUCT-004、REPORT-004 |
|
||||||
| 缺失事实 | 未知字段不当0,渠道失败不当无风险;AI不得创造不存在的额度/审批承诺 | PRODUCT-002、REPORT-003 |
|
| 缺失事实 | 未知字段不当0,渠道失败不当无风险;AI不得创造不存在的额度/审批承诺 | PRODUCT-002、REPORT-003 |
|
||||||
|
| 融资需求v2 | 金额/期限/用途校验、版本切换和旧键重放通过;历史NULL不补造需求 | ASSESS-001、ASSESS-003、ASSESS-004 |
|
||||||
|
| 完整度与准备等级 | 0与null、条件分母、四舍五入和一致性分开;未发布无分/等级;不等同授信 | REPORT-003、REPORT-004 |
|
||||||
|
| 匹配解释v2 | fail+unknown不推荐,证据可复算;不凑理由;配置错误不变用户不符;null rank/score合法 | PRODUCT-001、PRODUCT-002、PRODUCT-004、REPORT-003 |
|
||||||
| 私有资料 | 无登录/跨用户无法读取,撤销后不新增查询;授权版本可追溯 | FILE-004、DATA-004 |
|
| 私有资料 | 无登录/跨用户无法读取,撤销后不新增查询;授权版本可追溯 | FILE-004、DATA-004 |
|
||||||
| 课程进度 | 章节重排不串记录,重复完成幂等,换设备一致 | CONTENT-003 |
|
| 课程进度 | 章节重排不串记录,重复完成幂等,换设备一致 | CONTENT-003 |
|
||||||
| 积分并发 | 同日并发只奖励一次,流水与余额一致;复制/分享不视为实际邀请成功 | POINT-004 |
|
| 积分并发 | 同日并发只奖励一次,流水与余额一致;复制/分享不视为实际邀请成功 | POINT-004 |
|
||||||
|
|||||||
Binary file not shown.
+249
-68
@@ -1,6 +1,6 @@
|
|||||||
# AI 融资平台开发进度
|
# AI 融资平台开发进度
|
||||||
|
|
||||||
基线日期:2026-09-22。文档版本:1.0。技术方案和接口说明见[开发文档](./开发文档.md)。本台账用于记录真实开发与验收状态,任务编号与开发文档保持一致。
|
基线日期:2026-09-22。文档版本:1.4。最新实现见0.6节与6.6节;0.1—0.5保留此前阶段记录。技术方案和接口说明见[开发文档](./开发文档.md)。本台账用于记录真实开发与验收状态,任务编号与开发文档保持一致。
|
||||||
|
|
||||||
## 0. 2026-09-22 首轮实施更新(优先于初始基线)
|
## 0. 2026-09-22 首轮实施更新(优先于初始基线)
|
||||||
|
|
||||||
@@ -13,16 +13,93 @@
|
|||||||
| 后端回归 | 金额、加密、评估生命周期和账户基础回归通过 | tests/finance.php:41项通过,数据库回滚,Redis测试会话清理 |
|
| 后端回归 | 金额、加密、评估生命周期和账户基础回归通过 | tests/finance.php:41项通过,数据库回滚,Redis测试会话清理 |
|
||||||
| 真实HTTP | 密码登录、Bearer资料读取、退出、原密码改密和多个旧Token失效通过 | tests/api_session.php:16项通过,临时用户/会话清理;短信返回503 |
|
| 真实HTTP | 密码登录、Bearer资料读取、退出、原密码改密和多个旧Token失效通过 | tests/api_session.php:16项通过,临时用户/会话清理;短信返回503 |
|
||||||
| 前端基础 | 统一请求和错误、默认密码登录、Redis退出、userId缓存隔离 | services/api.uts、config.uts、storage.uts;真实业务其他接口待开发 |
|
| 前端基础 | 统一请求和错误、默认密码登录、Redis退出、userId缓存隔离 | services/api.uts、config.uts、storage.uts;真实业务其他接口待开发 |
|
||||||
| 浏览器联调 | 真实密码登录→首页→“我的”→确认退出;A/B账户草稿隔离;短信503无倒计时 | 本地Chromium,390×844和1280×900;详见6.1;不含微信内置浏览器 |
|
| 浏览器联调 | 真实密码登录→首页→“我的”→确认退出;A/B账户缓存隔离;短信503无倒计时;历史草稿检查已随功能取消 | 本地Chromium,390×844和1280×900;详见6.1;不含微信内置浏览器 |
|
||||||
| 编译 | Web编译通过;Android退出回调类型问题已修复后编译通过 | HBuilderX 5.26本地编译;非真机/正式发行验证 |
|
| 编译 | Web编译通过;Android退出回调类型问题已修复后编译通过 | HBuilderX 5.26本地编译;非真机/正式发行验证 |
|
||||||
| 外部准备 | 短信服务商、DeepSeek测试权限、首批真实产品资料均未具备 | AUTH-001、REPORT-001、PRODUCT-003保持阻塞;不虚构成功 |
|
| 外部准备 | 短信服务商和首批真实产品资料未具备;DeepSeek密钥与模型已收到并仅保存在后端私密配置 | AUTH-001、PRODUCT-003仍阻塞;REPORT-001配置已启动,真实调用与预算待验证 |
|
||||||
| 恢复点 | 已保存目标表结构与数据备份 | D:/AAdesk/202609/airz-backups/token-redis-20260922-165215-988442/user_token.sql |
|
| 恢复点 | 已保存目标表结构与数据备份 | D:/AAdesk/202609/airz-backups/token-redis-20260922-165215-988442/user_token.sql |
|
||||||
|
|
||||||
仍待完成:全量接口字典和requestId日志关联、生产配置校验、旧匿名草稿显式导入、敏感缓存清理、服务端完整profile/头像、短信注册/换绑及其他业务模块。当前没有将任何整模块标记为已完成。
|
仍待完成:全量接口字典和requestId日志关联、生产配置校验、短信注册/换绑及其他业务模块。当前没有将任何整模块标记为已完成。
|
||||||
|
|
||||||
|
### 0.1 最新调整:取消草稿、账户资料与AI配置
|
||||||
|
|
||||||
|
- 用户取消全部评估草稿:本机/云端保存、续填及旧草稿导入从范围移除。表单只在当前填写→确认流程中保留内存,离开、刷新和退出清空;历史档案仅面向正式提交记录,读取接口当时待实现;2026-09-23已接通,见0.2。
|
||||||
|
- 账户资料GET/PUT、头像POST已实现;服务端校验归属、类型、尺寸、500KB上限并重新编码。退出、过期及401清账户缓存;启动清理旧草稿键。
|
||||||
|
- 已撤回草稿接口及revision/client_draft_id/唯一索引;city_code、operating_months作为正式评估标准字段保留。撤回备份:D:/AAdesk/202609/airz-backups/cancel-drafts-20260922-180035/user_info.sql。
|
||||||
|
- DeepSeek密钥已存后端忽略的.env;两个模型已记录为deepseek-v4-pro-0813和deepseek-v4-flash-0731,接入腾讯云TokenHub。只完成配置与官方文档核对,未发起真实模型调用;预算、额度和模型权限待验证。文档不保存密钥。
|
||||||
|
- 最终代码验证:tests/api_profile.php 14项(含草稿路由移除)、tests/finance.php 41项通过;账户会话16项在本轮前段回归通过。中途39项草稿检查仅属已取消方案,不计当前功能交付。Web可运行,Android本地编译通过;并非真机或发行验收。
|
||||||
|
- 原69–117人日为初始估算基线;取消草稿后不再称作最新剩余工时,后续按正式提交与历史范围重估。60个任务编号不变,ASSESS-002用途更新为正式评估记录与历史。
|
||||||
|
|
||||||
|
|
||||||
|
### 0.2 2026-09-23 最新进展:正式提交与历史
|
||||||
|
|
||||||
|
- 完成POST /assessments(HTTP201固定接受回执)、GET列表/详情/状态;个人/企业正式输入保存至fa_user_info并创建唯一关联fa_ai_chat,不新增表/字段,不执行迁移。
|
||||||
|
- 无草稿、提交后只读;规范化白名单/金额/枚举/城市/月数校验、本人归属和脱敏。用户行锁+已有唯一索引防重复创建,原请求异内容409,软删除/内部draft不可读。
|
||||||
|
- 分析任务仍未接通:提交保存资料后明确记录failed/ANALYSIS_NOT_CONFIGURED,前端说明分析暂未开放;不会让记录假装排队或跳固定评分。DeepSeek尚未真实请求,短信和真实产品仍待准备。
|
||||||
|
- ASSESS-002进入待验收;ASSESS-003/004及REPORT-004进入开发中。提交与历史子流程可用,不将完整融资核心或AI报告标完成。
|
||||||
|
- 当前界面路径:填写→确认提交资料→真实状态→档案只读详情;历史空态、错误重试、每页20条加载更多、刷新及返回列表已验证。附件/外部授权未开放时明确阻止提交,选择不被静默丢弃。
|
||||||
|
|
||||||
|
### 0.3 2026-09-23 最新进展:DeepSeek、分析任务与失败重试
|
||||||
|
|
||||||
|
| 内容 | 当前结果 | 边界 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 模型 | Pro/Flash指定版本均真实连通;默认Flash已生成个人/企业自填报告 | 不自动回退;不代表商业预算或余额确认 |
|
||||||
|
| 分析任务 | 数据库预留→提交后Redis派发→独立worker→报告落库;并发抢占、补投及租约恢复 | 过期任务失败,不自动重复收费调用;生产守护未部署 |
|
||||||
|
| 重试与报告API | POST /assessments/:id/retry HTTP202;GET /assessments/:id/report;归属、原任务回执幂等、每日额度/次数上限 | 初次提交回执仍HTTP201不变;超时可能产生用量 |
|
||||||
|
| 报告与界面 | 真实事实/定性解释/缺失项;万元精确展示;失败重试、档案跳转、刷新恢复;移除固定85分与雷达 | 当前仅自填资料,overallScore=null,匹配明确未执行 |
|
||||||
|
| 安全与调用 | 白名单事实输入;不发姓名/手机号/证件/企业名/自由文本;日志仅安全错误与用量 | tokens未知为null,语义正确性仍需人工样例验收 |
|
||||||
|
| 数据库 | fa_user_info增补任务元数据和派发索引,无新表 | 迁移前评估0行;备份见开发文档及后端database/README |
|
||||||
|
| 测试 | 29项任务检查、15项隔离HTTP检查通过;原54项正式评估、41项基础、14项账户回归通过 | 54项在开启本地模型前运行;开启后该脚本会SKIP,不能算通过 |
|
||||||
|
| 浏览器/编译 | 本地普通Chromium H5,390×844/1280×900;企业真实重试→报告、精确金额及刷新恢复;Android编译成功 | 微信内H5、Safari、原生真机、生产容量未验收 |
|
||||||
|
|
||||||
|
开发默认策略:单用户每日20次预留,单评估最多3次,连接10秒/调用90秒,执行租约180秒,Redis reserved240秒。配额为可配置开发默认,商业策略和月预算仍待DEC-AI确认。金额只由服务端事实字段生成,中文/全角数值改写也会被报告校验拦截。测试所用合成账号会清理,原会员账号及密码未修改。
|
||||||
|
|
||||||
|
本轮不新增任务编号、不调整60项任务的基线工时。REPORT-001/002与ASSESS-003进入待验收;REPORT-003完整产品/外部数据范围仍阻塞,REPORT-004仅自填支线已联调,保持开发中。
|
||||||
|
|
||||||
|
|
||||||
|
### 0.4 2026-09-23 需求与评估结果契约补充(历史阶段:文档已完成,业务未实施)
|
||||||
|
|
||||||
|
新增[需求与评估结果数据契约](需求与评估结果数据契约.md):inputVersion=2需求字段、完整度字段集和公式、准备等级建议与发布闸门、准入三态和证据、报告resultVersion=2、历史兼容及任务快照。已参考旧说明书/原型,不覆盖用户后续确认的技术栈、无草稿和存储约定。
|
||||||
|
|
||||||
|
| 子范围 | 任务 | 本轮产物 | 新增业务状态/依赖 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 需求字段/版本/迁移 | ASSESS-001/002/003/004 | 四字段及DB映射、用途草案、旧键重放/历史NULL、新旧版本发布顺序 | 未开始;DEC-DEMAND待确认 |
|
||||||
|
| 完整度/准备等级 | REPORT-003/004 | 建议等权字段集、计数规则、一致性与非授信等级、未发布不出分 | 未开始;DEC-READINESS待确认 |
|
||||||
|
| 匹配解释/结果 | PRODUCT-001/002/003/004 | 三态与fail优先、单产品需求覆盖、证据/快照、分页/结果详情及null score/rank | 未开始;DEC-MATCH、真实产品仍阻塞 |
|
||||||
|
| 任务依据冻结 | ASSESS-003、REPORT-002/003 | analysis_context增量设计、提交时输入字典冻结、执行时策略/产品快照冻结与重试复用 | 未开始;随v2实施并验证 |
|
||||||
|
|
||||||
|
整体任务包含v1已实现与v2未开始两部分。ASSESS-002/003、REPORT-002由待验收调整为开发中,表示新增范围尚未完成;不撤销原v1测试证据。其他任务状态保持并补证据说明。文档审阅/静态校验不计业务验收,本轮无代码、数据库、模型请求或配置变更。
|
||||||
|
|
||||||
|
仍沿用60个编号和历史基线人日;本次增加的工作没有已确认增量估算,不能直接沿用69—117人日作为最新剩余工期。用途、完整度及等级建议先形成可审阅契约,确认后按任务重估;本轮不自动纳入渠道、注销、RAG、对比/PDF实现。
|
||||||
|
|
||||||
|
### 0.5 2026-09-24 融资需求录入、提交与历史(历史阶段)
|
||||||
|
|
||||||
|
| 子范围 | 当前结果 | 剩余边界 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| DEC-DEMAND | 用户授权实施契约用途/必填规则,已发布用途版本financing-purposes-20260923-v1,新建最低inputVersion=2 | 后台字典编辑尚未开发,变更配置须提升版本 |
|
||||||
|
| ASSESS-001/003 | 需求四字段、输入版本、严格校验、同键全字段比较、冻结用途字典/哈希、旧键重放已实现 | 私有附件/正式授权仍未接入,整体保持开发中 |
|
||||||
|
| ASSESS-002 | v2本人只读历史及冻结标签、v1“当时未采集”已实现并联调 | 改待验收;生产/业务验收待完成 |
|
||||||
|
| ASSESS-004 | 个人/企业第2步资金需求、确认编辑、提交及历史刷新、无草稿通过 | 微信内H5、Safari、真机及附件授权全链路未验收,整体开发中 |
|
||||||
|
| REPORT-002 | 提交input_snapshot与inputHash已实现,失败重试不覆盖 | execution_snapshot和报告v2策略/产品快照待做,整体开发中 |
|
||||||
|
| 数据库 | 2026-09-23备份后执行6列、3 CHECK共9变更,无新表;迁移前两表均0行,复跑预检0 DDL | 恢复点见开发文档0.5,不覆盖后续业务数据 |
|
||||||
|
| 报告边界 | inputVersion=2仍生成resultVersion=1自填报告;本轮用假模型验证,无真实模型调用 | DEC-READINESS、DEC-MATCH及真实产品仍待准备;需求尚不参与分析/匹配 |
|
||||||
|
|
||||||
|
专项76项(9月24日)及基础41项、任务29项、HTTP15项(9月23日)通过;H5手机/桌面、Web和Android编译证据见6.5。旧54项评估测试未作为本轮证据:它测试v1新建,在新门槛和分析开关下不适用。60个任务编号和历史基线人日保持不变,新增范围实际剩余工时待重估,不计算整体完成率。
|
||||||
|
|
||||||
|
### 0.6 2026-09-24 当前进展:完整度、准备状态与报告v2
|
||||||
|
|
||||||
|
| 子范围 | 实际结果 | 状态/剩余边界 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| DEC-READINESS | 按用户继续下一步授权,采用契约等权字段及三状态;开发版self-reported-base-20260924-v1已发布 | 业务人工样例及生产验收待完成 |
|
||||||
|
| REPORT-002 | 新提交冻结targetResultVersion,首次执行冻结策略和摘要,重试不改;旧目标保持v1 | 自填快照子项待验收;真实产品/规则/排序快照、生产守护未完成,整体开发中 |
|
||||||
|
| REPORT-003 | 个人14/企业9项、other条件分母、零值/none、invalid优先、整数舍入、依据引用、报告v2原子落库 | 自填子项已联调;真实产品、外部查询、v2真实模型语义仍待验收;完整范围仍阻塞 |
|
||||||
|
| REPORT-004 | 需求概况、逐项完整度、准备依据、AI证据标签、固定风险;v1和未发布策略兼容 | 手机/桌面H5通过;微信内/Safari/真机未验收,整体开发中 |
|
||||||
|
| 数据与环境 | 复用现有analysis_context和report_data,无迁移/新表;未处理现有会员任务 | 本轮只用假模型/合成数据,无真实模型调用,密钥配置未改 |
|
||||||
|
|
||||||
|
54项报告专项、76项需求、41项基础和15项HTTP通过;Web/Android本地编译通过,详见6.6。原29项任务回归保留9月23日证据,本轮未运行含全库恢复扫描的脚本,避免影响现有业务processing记录。60个任务编号和历史基线工时不变,不推算整体完成率。
|
||||||
|
|
||||||
## 1. 项目状态与维护规则
|
## 1. 项目状态与维护规则
|
||||||
|
|
||||||
当前已具备25个页面原型、部分本机交互、优化后的数据库结构和评估内部生命周期服务;密码账户基础已完成本地前后端联调,短信、评估、报告、匹配等完整业务仍待完成。AI供应商确定为DeepSeek,具体模型、预算、可用额度及测试权限待核验;其他服务商待定。
|
当前已具备25个页面原型、部分本机交互、优化后的数据库结构和评估内部生命周期服务;密码账户基础已完成本地前后端联调,短信、评估、报告、匹配等完整业务仍待完成。AI供应商确定为DeepSeek,模型已确定为deepseek-v4-pro-0813和deepseek-v4-flash-0731,两个模型权限及自填报告已真实验证,商业预算、余额及生产容量仍待核验;其他服务商待定。
|
||||||
|
|
||||||
一期目标是 **25 个页面全部接入真实业务,H5 优先上线**。短信、微信登录、一键登录、外部征信或数据查询、人工客服均为一期必做项,不能因为服务商未定而视为已完成或从首发范围中静默移除。贷款申请仅包括现有申请准备和资料引导,不含银行受理、授信、放款与还款交易。
|
一期目标是 **25 个页面全部接入真实业务,H5 优先上线**。短信、微信登录、一键登录、外部征信或数据查询、人工客服均为一期必做项,不能因为服务商未定而视为已完成或从首发范围中静默移除。贷款申请仅包括现有申请准备和资料引导,不含银行受理、授信、放款与还款交易。
|
||||||
|
|
||||||
@@ -45,7 +122,7 @@ P0 表示核心流程、安全、首发外部能力或发布门槛;P1 表示
|
|||||||
|
|
||||||
### 1.2 工时口径与更新规则
|
### 1.2 工时口径与更新规则
|
||||||
|
|
||||||
- 剩余工程量为 **69–117 人日**,按每周 5 个工作日约 **14–24 周**;不等于承诺上线日期,不含供应商开通、资质、审核与商务等待。
|
- 初始工程量基线为 **69–117 人日**(取消草稿及本轮实现后的实际剩余量待重估),按每周 5 个工作日约 **14–24 周**;不等于承诺上线日期,不含供应商开通、资质、审核与商务等待。
|
||||||
- 每类第三方暂按 1 个服务商估算;能力不足、增加渠道或调整业务范围时,修改受影响任务估算并在更新日志说明。
|
- 每类第三方暂按 1 个服务商估算;能力不足、增加渠道或调整业务范围时,修改受影响任务估算并在更新日志说明。
|
||||||
- 不用页面数、SQL 表数、代码行数计算业务完成率。文档完成也不计为下面 60 个业务任务完成。
|
- 不用页面数、SQL 表数、代码行数计算业务完成率。文档完成也不计为下面 60 个业务任务完成。
|
||||||
- 每次开发结束同步状态、实际证据、日期、依赖变化与下一步;保留原任务编号。任务跨模块时不得重复统计工时。
|
- 每次开发结束同步状态、实际证据、日期、依赖变化与下一步;保留原任务编号。任务跨模块时不得重复统计工时。
|
||||||
@@ -59,13 +136,13 @@ P0 表示核心流程、安全、首发外部能力或发布门槛;P1 表示
|
|||||||
| 模块 | 页面/交互 | 数据库 | 内部服务/后台 | 业务 API | 第三方 | 真实验收 |
|
| 模块 | 页面/交互 | 数据库 | 内部服务/后台 | 业务 API | 第三方 | 真实验收 |
|
||||||
| --- | --- | --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- | --- | --- |
|
||||||
| BASE 基础接口 | 请求已接本地API;首页仍为原型 | 通用框架表已有 | v1显式路由及环境配置已有 | Bearer、响应封装和401已接;requestId等待补 | 部署环境待定 | 本地账户基础通过,模块未验收 |
|
| BASE 基础接口 | 请求已接本地API;首页仍为原型 | 通用框架表已有 | v1显式路由及环境配置已有 | Bearer、响应封装和401已接;requestId等待补 | 部署环境待定 | 本地账户基础通过,模块未验收 |
|
||||||
| AUTH 账户安全 | 密码登录/退出/原密码改密已接入;资料本机缓存 | 用户唯一约束已有;Token仅存Redis | 旧密码兼容、加密和撤销基础已有 | 密码登录、GET资料、退出、原密码改密已实现;其他待开发 | 短信待定 | 密码基础通过,完整模块未验收 |
|
| AUTH 账户安全 | 密码登录/退出/原密码改密已接入;昵称/头像后端持久化 | 用户唯一约束已有;Token仅存Redis | 旧密码兼容、加密和撤销基础已有 | 密码登录、GET/PUT资料、头像上传、退出、原密码改密已实现;短信/换绑待开发 | 短信待定 | 密码基础通过,完整模块未验收 |
|
||||||
| LOGIN 第三方登录 | 微信/一键入口或占位行为 | 身份关联增量设计待做 | 渠道适配待开发 | 待开发 | 微信/一键服务待定 | 未完成 |
|
| LOGIN 第三方登录 | 微信/一键入口或占位行为 | 身份关联增量设计待做 | 渠道适配待开发 | 待开发 | 微信/一键服务待定 | 未完成 |
|
||||||
| FILE 资料授权 | 选图本机预览、授权勾选与占位说明 | documents/consent_data 仅预留 | 通用公共上传不能代替私有证件服务 | 私有上传、下载、授权事件待开发 | 私有存储待定 | 未完成 |
|
| FILE 资料授权 | 选图本机预览、授权勾选与占位说明 | documents/consent_data 仅预留 | 通用公共上传不能代替私有证件服务 | 私有上传、下载、授权事件待开发 | 私有存储待定 | 未完成 |
|
||||||
| ASSESS 评估档案 | 个人/企业草稿及流程已有 | 一次评估一条记录及状态/幂等字段已有 | Assessment 内部服务已有 | 草稿、提交、状态、历史待接入 | 依赖上传/授权/数据渠道 | 内部历史测试已有,真实闭环未完成 |
|
| ASSESS 评估档案 | 个人/企业填写流程已有;未提交不持久化 | 一次评估一条记录及状态/幂等字段已有 | Assessment 内部服务已有 | 正式提交、状态、历史API已接通;不提供草稿接口 | 依赖上传/授权/数据渠道 | 内部历史测试已有,真实闭环未完成 |
|
||||||
| DATA 外部查询 | 查询选项为意向或占位 | 查询事件持久化待增量设计 | 渠道、回调和对账待开发 | 待开发 | 数据渠道待定 | 未完成 |
|
| DATA 外部查询 | 查询选项为意向或占位 | 查询事件持久化待增量设计 | 渠道、回调和对账待开发 | 待开发 | 数据渠道待定 | 未完成 |
|
||||||
| PRODUCT 产品匹配 | 产品/匹配页使用模拟数据 | 范围字段、规则 JSON、版本/有效期已有 | 产品后台基础已有,规则执行器未完成 | 公共查询与评估匹配读取待开发 | 真实产品及规则待确认 | 结构/版本历史测试已有,业务匹配未验收 |
|
| PRODUCT 产品匹配 | 产品/匹配页使用模拟数据 | 范围字段、规则 JSON、版本/有效期已有 | 产品后台基础已有,规则执行器未完成 | 公共查询与评估匹配读取待开发 | 真实产品及规则待确认 | 结构/版本历史测试已有,业务匹配未验收 |
|
||||||
| REPORT AI 报告 | 分析/报告/雷达图为模拟结果 | report_data/match_result 和任务字段已有 | 生命周期有基础,AI Job/队列未接通 | 状态、报告读取和重试待开发 | DeepSeek 已选定,未接入 | 未完成 |
|
| REPORT AI 报告 | 已接真实自填报告,移除固定评分/雷达 | 报告快照、调用用量及任务元数据已接入 | Redis任务、补投/租约、重试已实现 | 自填报告支线已联调 | 指定Pro/Flash均真实可用 | 完整产品/查询范围未完成 |
|
||||||
| CHAT AI 助手 | 模拟对话 | 会话/消息/使用量字段已有 | 调用、上下文、配额待开发 | 待开发 | 依赖 DeepSeek | 未完成 |
|
| CHAT AI 助手 | 模拟对话 | 会话/消息/使用量字段已有 | 调用、上下文、配额待开发 | 待开发 | 依赖 DeepSeek | 未完成 |
|
||||||
| CONTENT 内容课堂 | 新闻/课程/搜索/FAQ 本地数据,学习进度本机 | 文章/融资课堂表已有,课程进度待增量设计 | 内容后台基础需补齐 | 真实列表/详情/进度/搜索待开发 | 内容运营素材待准备 | 未完成 |
|
| CONTENT 内容课堂 | 新闻/课程/搜索/FAQ 本地数据,学习进度本机 | 文章/融资课堂表已有,课程进度待增量设计 | 内容后台基础需补齐 | 真实列表/详情/进度/搜索待开发 | 内容运营素材待准备 | 未完成 |
|
||||||
| MSG 消息中心 | 静态消息及本机已读 | read_at/关联业务字段已有 | 通知事件/去重需补齐 | 列表/计数/已读待开发 | 无独立外部渠道要求 | 未完成 |
|
| MSG 消息中心 | 静态消息及本机已读 | read_at/关联业务字段已有 | 通知事件/去重需补齐 | 列表/计数/已读待开发 | 无独立外部渠道要求 | 未完成 |
|
||||||
@@ -97,18 +174,18 @@ P0 表示核心流程、安全、首发外部能力或发布门槛;P1 表示
|
|||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| 首页 | pages/home/home | BASE-004、CONTENT-002、MSG-003 | 原型/本地数据 |
|
| 首页 | pages/home/home | BASE-004、CONTENT-002、MSG-003 | 原型/本地数据 |
|
||||||
| 融资入口 | packages/finance/entry/entry | ASSESS-004 | 原型 |
|
| 融资入口 | packages/finance/entry/entry | ASSESS-004 | 原型 |
|
||||||
| 评估填写 | packages/finance/assessment/assessment | ASSESS-004、FILE-004、DATA-004 | 本机草稿/图片预览 |
|
| 评估填写 | packages/finance/assessment/assessment | ASSESS-004、FILE-004、DATA-004 | 当前流程内存填写/本机图片预览 |
|
||||||
| 评估确认 | packages/finance/confirm/confirm | ASSESS-004 | 本机金额确认 |
|
| 评估确认 | packages/finance/confirm/confirm | ASSESS-004 | 元/万元转换、正式提交及网络重试幂等 |
|
||||||
| 分析中 | packages/finance/analyzing/analyzing | REPORT-004 | 模拟计时与跳转 |
|
| 分析中 | packages/finance/analyzing/analyzing | REPORT-004 | 按assessmentId轮询状态、失败重试、成功后打开真实报告 |
|
||||||
| 评估报告 | packages/finance/report/report | REPORT-004 | 模拟报告 |
|
| 评估报告 | packages/finance/report/report | REPORT-004 | 按assessmentId读取真实自填报告、金额万元,无固定评分 |
|
||||||
| 匹配结果 | packages/finance/matches/matches | PRODUCT-004、REPORT-004 | 模拟匹配 |
|
| 匹配结果 | packages/finance/matches/matches | PRODUCT-004、REPORT-004 | 模拟匹配 |
|
||||||
| 产品详情 | packages/finance/product/product | PRODUCT-004 | 模拟产品/申请准备 |
|
| 产品详情 | packages/finance/product/product | PRODUCT-004 | 模拟产品/申请准备 |
|
||||||
| AI 助手 | packages/finance/assistant/assistant | CHAT-004 | 模拟对话 |
|
| AI 助手 | packages/finance/assistant/assistant | CHAT-004 | 模拟对话 |
|
||||||
| 融资档案 | packages/finance/archive/archive | ASSESS-004、REPORT-004 | 本机档案 |
|
| 融资档案 | packages/finance/archive/archive | ASSESS-004、REPORT-004 | 本人历史分页、脱敏只读详情、刷新恢复 |
|
||||||
| 启动引导 | packages/account/index/index | BASE-003、AUTH-002 | 原型/本机引导 |
|
| 启动引导 | packages/account/index/index | BASE-003、AUTH-002 | 原型/本机引导 |
|
||||||
| 登录 | packages/account/login/login | AUTH-002、LOGIN-004 | 未完成真实认证 |
|
| 登录 | packages/account/login/login | AUTH-002、LOGIN-004 | 未完成真实认证 |
|
||||||
| 个人中心 | packages/account/profile/profile | AUTH-003、POINT-004、MSG-003 | 本机资料/入口 |
|
| 个人中心 | packages/account/profile/profile | AUTH-003、POINT-004、MSG-003 | 服务端昵称头像/入口 |
|
||||||
| 修改资料 | packages/account/edit-profile/edit-profile | AUTH-003 | 昵称/头像本机保存 |
|
| 修改资料 | packages/account/edit-profile/edit-profile | AUTH-003 | 昵称/头像已接服务端持久化 |
|
||||||
| 账户安全 | packages/account/security/security | AUTH-004 | 入口与客户端校验 |
|
| 账户安全 | packages/account/security/security | AUTH-004 | 入口与客户端校验 |
|
||||||
| 修改密码 | packages/account/password/password | AUTH-004 | 真实服务未接通,不模拟成功 |
|
| 修改密码 | packages/account/password/password | AUTH-004 | 真实服务未接通,不模拟成功 |
|
||||||
| 换绑手机 | packages/account/change-phone/change-phone | AUTH-004 | 真实服务未接通,不模拟成功 |
|
| 换绑手机 | packages/account/change-phone/change-phone | AUTH-004 | 真实服务未接通,不模拟成功 |
|
||||||
@@ -129,8 +206,8 @@ P0 表示核心流程、安全、首发外部能力或发布门槛;P1 表示
|
|||||||
| --- | --- | --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- | --- | --- |
|
||||||
| M0 契约与接入准备 | BASE;同步发起全部外部资源和业务决策收集 | 3–5 | 3–5 | 契约、认证/响应、环境和缓存隔离可用;首页骨架具备联调入口;依赖有责任人与清单 | 未开始 | 待记录 / 待记录 |
|
| M0 契约与接入准备 | BASE;同步发起全部外部资源和业务决策收集 | 3–5 | 3–5 | 契约、认证/响应、环境和缓存隔离可用;首页骨架具备联调入口;依赖有责任人与清单 | 未开始 | 待记录 / 待记录 |
|
||||||
| M1 账户与资料闭环 | AUTH、LOGIN、FILE | 14–25 | 17–30 | 真实账户、账户安全、微信/一键支持场景、私有上传与版本授权验收完成 | 未开始,存在直接外部阻塞 | 待记录 / 待记录 |
|
| M1 账户与资料闭环 | AUTH、LOGIN、FILE | 14–25 | 17–30 | 真实账户、账户安全、微信/一键支持场景、私有上传与版本授权验收完成 | 未开始,存在直接外部阻塞 | 待记录 / 待记录 |
|
||||||
| M2 融资核心闭环 | ASSESS、DATA、PRODUCT、REPORT | 25–41 | 42–71 | 个人/企业各完成真实评估、授权查询、确定性匹配、DeepSeek 报告与历史;失败恢复可用 | 未开始,存在外部/业务决策阻塞 | 待记录 / 待记录 |
|
| M2 融资核心闭环 | ASSESS、DATA、PRODUCT、REPORT | 25–41 | 42–71 | 个人/企业各完成真实评估、授权查询、确定性匹配、DeepSeek 报告与历史;失败恢复可用 | 开发中,正式提交/历史/自填分析已联调;产品和外部渠道仍阻塞 | 待记录 / 待记录 |
|
||||||
| M3 全部页面业务化 | CHAT、CONTENT、MSG、POINT、SUPPORT、ADMIN | 20–35 | 62–106 | 助手、内容、课堂、搜索、消息、积分邀请、反馈和人工客服及运营后台验收通过 | 未开始,存在外部/业务决策阻塞 | 待记录 / 待记录 |
|
| M3 全部页面业务化 | CHAT、CONTENT、MSG、POINT、SUPPORT、ADMIN | 20–35 | 62–106 | 助手、内容、课堂、搜索、消息、积分邀请、反馈和人工客服及运营后台验收通过 | 未开始;现有页面/部分本机功能为基础,真实业务按对应任务推进 | 待记录 / 待记录 |
|
||||||
| M4 H5 验收上线 | RELEASE | 7–11 | 69–117 | 全部一期任务验收;普通 H5/微信内 H5 与渠道支持矩阵通过;部署恢复演练和发布记录完整 | 未开始,部署资源待定 | 待记录 / 待记录 |
|
| M4 H5 验收上线 | RELEASE | 7–11 | 69–117 | 全部一期任务验收;普通 H5/微信内 H5 与渠道支持矩阵通过;部署恢复演练和发布记录完整 | 未开始,部署资源待定 | 待记录 / 待记录 |
|
||||||
|
|
||||||
后续多端里程碑不计入上述 69–117 人日,也不属于一期 H5 验收:微信小程序需平台 AppID、隐私配置、授权/支付无关能力复核、包体与真机测试和提审;Android 需签名、真实设备与网络/权限适配、渠道材料;iOS 需 macOS/Xcode、签名、真实设备、隐私材料和审核。具体范围、顺序与工时在 H5 稳定后另行确认,不能拿历史本地编译代替发布。
|
后续多端里程碑不计入上述 69–117 人日,也不属于一期 H5 验收:微信小程序需平台 AppID、隐私配置、授权/支付无关能力复核、包体与真机测试和提审;Android 需签名、真实设备与网络/权限适配、渠道材料;iOS 需 macOS/Xcode、签名、真实设备、隐私材料和审核。具体范围、顺序与工时在 H5 稳定后另行确认,不能拿历史本地编译代替发布。
|
||||||
@@ -145,19 +222,19 @@ P0 表示核心流程、安全、首发外部能力或发布门槛;P1 表示
|
|||||||
|
|
||||||
| 任务编号 | 任务 | 范围 | 优先级 | 直接依赖 | 预计人日 | 状态 |
|
| 任务编号 | 任务 | 范围 | 优先级 | 直接依赖 | 预计人日 | 状态 |
|
||||||
| --- | --- | --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- | --- | --- |
|
||||||
| BASE-001 | 接口契约与字段字典 | 定义版本路由、响应、错误、分页、ID、金额、时间及接口状态;确认前后端字段映射。 | P0 | 无内部前置 | 0.5–1 | 开发中 |
|
| BASE-001 | 接口契约与字段字典 | 定义版本路由、响应、错误、分页、ID、金额、时间及接口状态;确认前后端字段映射。 | P0 | 无 | 0.5–1 | 开发中 |
|
||||||
| BASE-002 | 统一请求与认证适配 | 统一共享/账户请求;后端识别 Bearer;解析业务失败和401;限定 CORS;记录 requestId。 | P0 | BASE-001 | 1–1.5 | 开发中 |
|
| BASE-002 | 统一请求与认证适配 | 统一共享/账户请求;后端识别 Bearer;解析业务失败和401;限定 CORS;记录 requestId。 | P0 | BASE-001 | 1–1.5 | 开发中 |
|
||||||
| BASE-003 | 环境与用户缓存隔离 | 开发/测试/生产配置;生产禁用 mock;会话和草稿等缓存按不可变 userId 隔离。 | P0 | BASE-002 | 1–1.5 | 开发中 |
|
| BASE-003 | 环境与用户缓存隔离 | 开发/测试/生产配置;生产禁用 mock;会话等账户缓存按不可变 userId 隔离;未提交表单只存当前流程内存。 | P0 | BASE-002 | 1–1.5 | 开发中 |
|
||||||
| BASE-004 | 首页和联调骨架 | 首页接入聚合数据、加载/空态/失败重试及登录回跳;建立接口冒烟入口。 | P1 | BASE-002、BASE-003 | 0.5–1 | 未开始 |
|
| BASE-004 | 首页和联调骨架 | 首页接入聚合数据、加载/空态/失败重试及登录回跳;建立接口冒烟入口。 | P1 | BASE-002、BASE-003 | 0.5–1 | 未开始 |
|
||||||
|
|
||||||
| 任务编号 | 负责人 | 验收条件 | 证据 | 更新时间 |
|
| 任务编号 | 负责人 | 验收条件 | 证据 | 更新时间 |
|
||||||
| --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- |
|
||||||
| BASE-001 | 全栈开发(待指定) | 接口样例可同时被 PHP 和 UTS 解析;区分占位接口与后端现有实现。 | 已实施子集和验证见第0节;完整任务未验收 | 2026-09-22 |
|
| BASE-001 | 全栈开发(待指定) | 接口样例可同时被 PHP 和 UTS 解析;区分占位接口与后端现有实现。 | 已实施子集和验证见第0节;完整任务未验收 | 2026-09-22 |
|
||||||
| BASE-002 | 全栈开发(待指定) | HTTP 200 中业务失败不判成功;过期 Token 清会话;普通业务按所有者鉴权。 | 已实施子集和验证见第0节;完整任务未验收 | 2026-09-22 |
|
| BASE-002 | 全栈开发(待指定) | HTTP 200 中业务失败不判成功;过期 Token 清会话;普通业务按所有者鉴权。 | 已实施子集和验证见第0节;完整任务未验收 | 2026-09-22 |
|
||||||
| BASE-003 | 全栈开发(待指定) | 切换账户无串数据;匿名旧草稿仅在用户明确选择导入后绑定;退出清理敏感缓存。 | 已实施子集和验证见第0节;完整任务未验收 | 2026-09-22 |
|
| BASE-003 | 全栈开发(待指定) | 切换账户无串数据;旧草稿键清理且不恢复;退出清理敏感缓存和临时填写内容。 | 已实施子集和验证见第0节;完整任务未验收 | 2026-09-22 |
|
||||||
| BASE-004 | 全栈开发(待指定) | 深链接与刷新正确恢复;首页无硬编码账户数据;真实未读数后续由 MSG 接入。 | 未验收;无真实业务验收证据 | 2026-09-22 |
|
| BASE-004 | 全栈开发(待指定) | 深链接与刷新正确恢复;首页无硬编码账户数据;真实未读数后续由 MSG 接入。 | 未验收;无真实业务验收证据 | 2026-09-22 |
|
||||||
|
|
||||||
直接外部/业务阻塞:无;仍须满足前置任务。
|
DEC-DEMAND已确认并发布,需求录入/提交/历史子范围已实现;其他既有前置任务不变。
|
||||||
|
|
||||||
### AUTH|账户与短信安全
|
### AUTH|账户与短信安全
|
||||||
|
|
||||||
@@ -165,16 +242,16 @@ P0 表示核心流程、安全、首发外部能力或发布门槛;P1 表示
|
|||||||
|
|
||||||
| 任务编号 | 任务 | 范围 | 优先级 | 直接依赖 | 预计人日 | 状态 |
|
| 任务编号 | 任务 | 范围 | 优先级 | 直接依赖 | 预计人日 | 状态 |
|
||||||
| --- | --- | --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- | --- | --- |
|
||||||
| AUTH-001 | 短信接入与验证码服务 | 供应商适配、签名模板、验证码用途/有效期/频控/核销、测试发送。 | P0 | BASE-002 | 1–2 | 阻塞 |
|
| AUTH-001 | 短信接入与验证码服务 | 供应商适配、签名模板、验证码用途/有效期/频控/核销、测试发送。 | P0 | BASE-002、EXT-SMS | 1–2 | 阻塞 |
|
||||||
| AUTH-002 | 真实登录与会话 | 短信登录/首次注册、密码登录、资料读取、登出、禁用账户、旧密码散列升级。 | P0 | AUTH-001、BASE-003 | 1.5–2 | 开发中 |
|
| AUTH-002 | 真实登录与会话 | 短信登录/首次注册、密码登录、资料读取、登出、禁用账户、旧密码散列升级。 | P0 | AUTH-001、BASE-003 | 1.5–2 | 开发中 |
|
||||||
| AUTH-003 | 资料与头像持久化 | 昵称、头像 API 和文件校验;以服务端 profile 为准,迁移本机回显逻辑。 | P1 | AUTH-002 | 1–2 | 未开始 |
|
| AUTH-003 | 资料与头像持久化 | 昵称、头像 API 和文件校验;以服务端 profile 为准,迁移本机回显逻辑。 | P1 | AUTH-002 | 1–2 | 开发中 |
|
||||||
| AUTH-004 | 改密与手机换绑 | 登录校验、原/新手机双挑战、号码占用、事务及全部会话撤销;修复旧改绑免登录声明。 | P0 | AUTH-002 | 1.5–2 | 开发中 |
|
| AUTH-004 | 改密与手机换绑 | 登录校验、原/新手机双挑战、号码占用、事务及全部会话撤销;修复旧改绑免登录声明。 | P0 | AUTH-002 | 1.5–2 | 开发中 |
|
||||||
|
|
||||||
| 任务编号 | 负责人 | 验收条件 | 证据 | 更新时间 |
|
| 任务编号 | 负责人 | 验收条件 | 证据 | 更新时间 |
|
||||||
| --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- |
|
||||||
| AUTH-001 | 全栈开发(待指定) | 发送成功才倒计时;验证码过期、复用、跨用途和频繁发送均拒绝。 | 未验收;无真实业务验收证据 | 2026-09-22 |
|
| AUTH-001 | 全栈开发(待指定) | 发送成功才倒计时;验证码过期、复用、跨用途和频繁发送均拒绝。 | 未验收;无真实业务验收证据 | 2026-09-22 |
|
||||||
| AUTH-002 | 全栈开发(待指定) | 真实账户登录成功;登录失败无假 Token;登出撤销当前 Token;旧散列兼容。 | 已实施子集和验证见第0节;完整任务未验收 | 2026-09-22 |
|
| AUTH-002 | 全栈开发(待指定) | 真实账户登录成功;登录失败无假 Token;登出撤销当前 Token;旧散列兼容。 | 已实施子集和验证见第0节;完整任务未验收 | 2026-09-22 |
|
||||||
| AUTH-003 | 全栈开发(待指定) | 不同设备资料一致;昵称不覆盖评估实名;头像不依赖临时路径。 | 未验收;无真实业务验收证据 | 2026-09-22 |
|
| AUTH-003 | 全栈开发(待指定) | 不同设备资料一致;昵称不覆盖评估实名;头像不依赖临时路径。 | 本地14项HTTP及昵称/头像持久化交互通过;正式环境跨设备验收待补 | 2026-09-22 |
|
||||||
| AUTH-004 | 全栈开发(待指定) | 跨账户或单个验证码不能换绑;修改后旧 Token 全失效;新密码8–32位且含字母和数字。 | 已实施子集和验证见第0节;完整任务未验收 | 2026-09-22 |
|
| AUTH-004 | 全栈开发(待指定) | 跨账户或单个验证码不能换绑;修改后旧 Token 全失效;新密码8–32位且含字母和数字。 | 已实施子集和验证见第0节;完整任务未验收 | 2026-09-22 |
|
||||||
|
|
||||||
直接阻塞:AUTH-001 → EXT-SMS。
|
直接阻塞:AUTH-001 → EXT-SMS。
|
||||||
@@ -185,9 +262,9 @@ P0 表示核心流程、安全、首发外部能力或发布门槛;P1 表示
|
|||||||
|
|
||||||
| 任务编号 | 任务 | 范围 | 优先级 | 直接依赖 | 预计人日 | 状态 |
|
| 任务编号 | 任务 | 范围 | 优先级 | 直接依赖 | 预计人日 | 状态 |
|
||||||
| --- | --- | --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- | --- | --- |
|
||||||
| LOGIN-001 | 登录渠道能力与身份方案 | 确认微信内/普通H5方式、号码认证H5能力;定义渠道身份与手机号账户绑定方案。 | P0 | BASE-001 | 1–2 | 阻塞 |
|
| LOGIN-001 | 登录渠道能力与身份方案 | 确认微信内/普通H5方式、号码认证H5能力;定义渠道身份与手机号账户绑定方案。 | P0 | BASE-001、EXT-WX、EXT-ONE | 1–2 | 阻塞 |
|
||||||
| LOGIN-002 | 微信登录接入 | 授权发起、state校验、code服务端交换、首次手机号绑定与已有账户冲突处理。 | P0 | LOGIN-001、AUTH-002 | 1.5–3 | 阻塞 |
|
| LOGIN-002 | 微信登录接入 | 授权发起、state校验、code服务端交换、首次手机号绑定与已有账户冲突处理。 | P0 | LOGIN-001、AUTH-002、EXT-WX | 1.5–3 | 阻塞 |
|
||||||
| LOGIN-003 | 一键登录接入 | 号码认证SDK/网页流程、服务端验证、能力检测、账号绑定、短信回退。 | P0 | LOGIN-001、AUTH-002 | 1.5–3 | 阻塞 |
|
| LOGIN-003 | 一键登录接入 | 号码认证SDK/网页流程、服务端验证、能力检测、账号绑定、短信回退。 | P0 | LOGIN-001、AUTH-002、EXT-ONE | 1.5–3 | 阻塞 |
|
||||||
| LOGIN-004 | 身份渠道联调 | 微信内、普通移动H5和桌面H5分别验证;解绑/冲突与失败恢复按已选渠道能力实现。 | P0 | LOGIN-002、LOGIN-003 | 1–2 | 未开始 |
|
| LOGIN-004 | 身份渠道联调 | 微信内、普通移动H5和桌面H5分别验证;解绑/冲突与失败恢复按已选渠道能力实现。 | P0 | LOGIN-002、LOGIN-003 | 1–2 | 未开始 |
|
||||||
|
|
||||||
| 任务编号 | 负责人 | 验收条件 | 证据 | 更新时间 |
|
| 任务编号 | 负责人 | 验收条件 | 证据 | 更新时间 |
|
||||||
@@ -205,9 +282,9 @@ P0 表示核心流程、安全、首发外部能力或发布门槛;P1 表示
|
|||||||
|
|
||||||
| 任务编号 | 任务 | 范围 | 优先级 | 直接依赖 | 预计人日 | 状态 |
|
| 任务编号 | 任务 | 范围 | 优先级 | 直接依赖 | 预计人日 | 状态 |
|
||||||
| --- | --- | --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- | --- | --- |
|
||||||
| FILE-001 | 私有资料存储方案 | 区分公开头像与私有证件;确定文件白名单、大小限制、存储密钥、附件元数据与保留策略。 | P0 | BASE-001 | 1–1.5 | 阻塞 |
|
| FILE-001 | 私有资料存储方案 | 区分公开头像与私有证件;确定文件白名单、大小限制、存储密钥、附件元数据与保留策略。 | P0 | BASE-001、EXT-STORAGE、DEC-PRIVACY | 1–1.5 | 阻塞 |
|
||||||
| FILE-002 | 私有上传与下载 | 持久附件ID、归属校验、大小/MIME/内容校验、鉴权读取、失败重试和删除策略。 | P0 | FILE-001、AUTH-002 | 1–2 | 未开始 |
|
| FILE-002 | 私有上传与下载 | 持久附件ID、归属校验、大小/MIME/内容校验、鉴权读取、失败重试和删除策略。 | P0 | FILE-001、AUTH-002 | 1–2 | 未开始 |
|
||||||
| FILE-003 | 协议与授权版本 | 协议全文展示、同意/撤销事件、范围与协议版本、历史快照及外部授权凭据边界。 | P0 | FILE-001、AUTH-002 | 1–2 | 阻塞 |
|
| FILE-003 | 协议与授权版本 | 协议全文展示、同意/撤销事件、范围与协议版本、历史快照及外部授权凭据边界。 | P0 | FILE-001、AUTH-002、DEC-PRIVACY | 1–2 | 阻塞 |
|
||||||
| FILE-004 | 资料授权页面联调 | 替换选图预览和占位协议;展示上传/授权状态;拒绝、取消、断网恢复。 | P0 | FILE-002、FILE-003 | 1–1.5 | 未开始 |
|
| FILE-004 | 资料授权页面联调 | 替换选图预览和占位协议;展示上传/授权状态;拒绝、取消、断网恢复。 | P0 | FILE-002、FILE-003 | 1–1.5 | 未开始 |
|
||||||
|
|
||||||
| 任务编号 | 负责人 | 验收条件 | 证据 | 更新时间 |
|
| 任务编号 | 负责人 | 验收条件 | 证据 | 更新时间 |
|
||||||
@@ -225,17 +302,17 @@ P0 表示核心流程、安全、首发外部能力或发布门槛;P1 表示
|
|||||||
|
|
||||||
| 任务编号 | 任务 | 范围 | 优先级 | 直接依赖 | 预计人日 | 状态 |
|
| 任务编号 | 任务 | 范围 | 优先级 | 直接依赖 | 预计人日 | 状态 |
|
||||||
| --- | --- | --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- | --- | --- |
|
||||||
| ASSESS-001 | 评估字段与增量模型 | 确定枚举、地区代码、经营月数、附件引用、授权快照和前后端完整字段映射。 | P0 | BASE-001 | 1–2 | 未开始 |
|
| ASSESS-001 | 评估字段与增量模型 | 统一现有字典并补融资需求四字段、inputVersion及增量列;用途字典/必填规则按专项契约确认,保留附件和授权映射。 | P0 | BASE-001、DEC-DEMAND | 1–2 | 开发中 |
|
||||||
| ASSESS-002 | 云端草稿与档案 | 新建/保存/读取草稿、版本冲突、列表、复制既往资料新建评估及用户隔离。 | P0 | ASSESS-001、AUTH-002 | 1–2 | 未开始 |
|
| ASSESS-002 | 正式评估记录与历史 | 本人历史分页/只读详情及用户隔离;补v1/v2需求回显与旧未采集提示,无草稿保存或续填。 | P0 | ASSESS-001、AUTH-002 | 1–2 | 待验收 |
|
||||||
| ASSESS-003 | 提交与生命周期API | 接入submit/owned/retry,服务端校验;持久保存每次提交/重试请求键、参数摘要与taskId,返回原接受响应。 | P0 | ASSESS-002、FILE-004 | 1.5–2 | 未开始 |
|
| ASSESS-003 | 提交与生命周期API | 保留提交201及重试202幂等;补inputVersion切换、新需求校验、输入字典冻结、同键全字段比较和旧请求重放兼容。 | P0 | ASSESS-002、FILE-004 | 1.5–2 | 开发中 |
|
||||||
| ASSESS-004 | 评估全页面联调 | 评估/确认/分析/档案传递assessmentId;处理金额、退出续填、并发及历史跳转。 | P0 | ASSESS-003 | 1.5–2 | 未开始 |
|
| ASSESS-004 | 评估全页面联调 | 评估/确认/分析/报告/档案贯穿assessmentId;新增资金需求分组和只读展示,保留金额、清理与并发回归。 | P0 | ASSESS-003 | 1.5–2 | 开发中 |
|
||||||
|
|
||||||
| 任务编号 | 负责人 | 验收条件 | 证据 | 更新时间 |
|
| 任务编号 | 负责人 | 验收条件 | 证据 | 更新时间 |
|
||||||
| --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- |
|
||||||
| ASSESS-001 | 全栈开发(待指定) | 未知与0区分;个人/企业必填口径明确;新增字段列独立迁移,不重建结果表。 | 未验收;无真实业务验收证据 | 2026-09-22 |
|
| ASSESS-001 | 全栈开发(待指定) | 现有与v2字段映射明确,元字符串/整数月/用途条件校验;历史新字段NULL,不新建评估表。 | 需求四字段/版本/用途字典及迁移已实现;金额/期限/other/信用卡一致性含于76项检查;私有附件与正式授权仍待接入 | 2026-09-24 |
|
||||||
| ASSESS-002 | 全栈开发(待指定) | 同用户可有多次同类型评估;不同账号不能读写;历史输入不被编辑覆盖。 | 未验收;无真实业务验收证据 | 2026-09-22 |
|
| ASSESS-002 | 全栈开发(待指定) | 同用户可多次评估;越权拒绝;历史v1不回填需求、不重算,v2显示冻结需求且不可编辑。 | v1未采集/v2冻结需求只读回显、HTTP越权检查及个人/企业H5刷新通过;待业务验收,旧54项仅作历史证据 | 2026-09-24 |
|
||||||
| ASSESS-003 | 全栈开发(待指定) | 同requestId只提交一次;多次重试后回放旧键仍返回原taskId;旧任务不可回写,完成方法不对客户端开放。 | 未验收;无真实业务验收证据 | 2026-09-22 |
|
| ASSESS-003 | 全栈开发(待指定) | 同键同内容原回执,合法需求或版本变化409、非法格式422;最低新建版本2或用途停用不阻断原键回放;旧重试返回原taskId。 | 76项覆盖新建最低2、旧v1键/旧用途回放、全字段冲突、并发唯一记录、重试原回执和输入快照不变;附件授权待接 | 2026-09-24 |
|
||||||
| ASSESS-004 | 全栈开发(待指定) | 同一评估ID贯穿流程;0.800001万元精确提交为8000.01元;处理中不显示假报告。 | 未验收;无真实业务验收证据 | 2026-09-22 |
|
| ASSESS-004 | 全栈开发(待指定) | 个人/企业需求均能提交及回显;12.345678万元=123456.78元;无草稿,历史不串账户,处理中无假报告。 | H5个人12.345678万元及企业0.000001万元正确提交/刷新;确认编辑、other清理、字典失败恢复、刷新清空通过;真实渠道待验收 | 2026-09-24 |
|
||||||
|
|
||||||
直接外部/业务阻塞:无;仍须满足前置任务。
|
直接外部/业务阻塞:无;仍须满足前置任务。
|
||||||
|
|
||||||
@@ -245,7 +322,7 @@ P0 表示核心流程、安全、首发外部能力或发布门槛;P1 表示
|
|||||||
|
|
||||||
| 任务编号 | 任务 | 范围 | 优先级 | 直接依赖 | 预计人日 | 状态 |
|
| 任务编号 | 任务 | 范围 | 优先级 | 直接依赖 | 预计人日 | 状态 |
|
||||||
| --- | --- | --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- | --- | --- |
|
||||||
| DATA-001 | 查询渠道与范围定稿 | 确认实际可开通数据产品、个人/企业范围、授权凭证、测试样例、计费与返回解释。 | P0 | BASE-001 | 1–2 | 阻塞 |
|
| DATA-001 | 查询渠道与范围定稿 | 确认实际可开通数据产品、个人/企业范围、授权凭证、测试样例、计费与返回解释。 | P0 | BASE-001、EXT-DATA、DEC-QUERY | 1–2 | 阻塞 |
|
||||||
| DATA-002 | 授权查询适配与记录 | 服务端发起、请求唯一键、渠道凭证隔离、查询事件持久化及assessmentId绑定。 | P0 | DATA-001、FILE-003、ASSESS-003 | 2–4 | 未开始 |
|
| DATA-002 | 授权查询适配与记录 | 服务端发起、请求唯一键、渠道凭证隔离、查询事件持久化及assessmentId绑定。 | P0 | DATA-001、FILE-003、ASSESS-003 | 2–4 | 未开始 |
|
||||||
| DATA-003 | 回调与结果标准化 | 验签、防重放、回调幂等、主动查询对账、结果字段/来源/时间映射与受限留存。 | P0 | DATA-002 | 2–4 | 未开始 |
|
| DATA-003 | 回调与结果标准化 | 验签、防重放、回调幂等、主动查询对账、结果字段/来源/时间映射与受限留存。 | P0 | DATA-002 | 2–4 | 未开始 |
|
||||||
| DATA-004 | 异常与渠道端到端验收 | 超时、撤销、拒绝、余额不足、重复回调、重查和H5往返恢复。 | P0 | DATA-003 | 1–2 | 未开始 |
|
| DATA-004 | 异常与渠道端到端验收 | 超时、撤销、拒绝、余额不足、重复回调、重查和H5往返恢复。 | P0 | DATA-003 | 1–2 | 未开始 |
|
||||||
@@ -265,17 +342,17 @@ P0 表示核心流程、安全、首发外部能力或发布门槛;P1 表示
|
|||||||
|
|
||||||
| 任务编号 | 任务 | 范围 | 优先级 | 直接依赖 | 预计人日 | 状态 |
|
| 任务编号 | 任务 | 范围 | 优先级 | 直接依赖 | 预计人日 | 状态 |
|
||||||
| --- | --- | --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- | --- | --- |
|
||||||
| PRODUCT-001 | 规则字典与评分口径 | 统一硬准入、未知值处理、补充规则白名单、匹配评分和理由;准备人工确认样例。 | P0 | ASSESS-001 | 1.5–2 | 阻塞 |
|
| PRODUCT-001 | 规则字典与评分口径 | 确定准入三态、来源要求、单产品需求覆盖、用途规则、证据结构和版本化排序;评分未发布时null。 | P0 | ASSESS-001、DEC-MATCH | 1.5–2 | 阻塞 |
|
||||||
| PRODUCT-002 | 准入与排序执行器 | 筛选上架/有效期/地区/对象及硬条件;执行补充规则、排序并输出可解释理由。 | P0 | PRODUCT-001 | 2–3 | 未开始 |
|
| PRODUCT-002 | 准入与排序执行器 | 执行上架/有效期/对象/地区及需求硬规则;保存pass/fail/unknown证据,生成eligibility和稳定rank。 | P0 | PRODUCT-001 | 2–3 | 未开始 |
|
||||||
| PRODUCT-003 | 产品后台与公共查询 | 补齐规则编辑校验、发布预检、产品列表/详情与范围字段;录入真实测试产品。 | P0 | PRODUCT-001、BASE-002 | 1.5–2 | 阻塞 |
|
| PRODUCT-003 | 产品后台与公共查询 | 补规则条件编辑、来源/用途依据、发布预检、版本与公共详情;录入真实产品,缺依据阻止发布。 | P0 | PRODUCT-001、BASE-002、DEC-PRODUCT | 1.5–2 | 阻塞 |
|
||||||
| PRODUCT-004 | 匹配页面与快照 | 独立MatchResult类型、额度/期限/利率范围、匹配列表排序、产品分享与申请准备。 | P0 | PRODUCT-002、PRODUCT-003、ASSESS-004 | 1–2 | 未开始 |
|
| PRODUCT-004 | 匹配页面与快照 | 按assessmentId展示三态分组、最多三个候选及分页;结果详情展示依据/注意项/AI解释,保留产品快照与申请准备。 | P0 | PRODUCT-002、PRODUCT-003、ASSESS-004 | 1–2 | 未开始 |
|
||||||
|
|
||||||
| 任务编号 | 负责人 | 验收条件 | 证据 | 更新时间 |
|
| 任务编号 | 负责人 | 验收条件 | 证据 | 更新时间 |
|
||||||
| --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- |
|
||||||
| PRODUCT-001 | 全栈开发(待指定) | 版本化规则与样例可审阅;地区/经营时间可确定比较;规则不执行任意表达式。 | 未验收;无真实业务验收证据 | 2026-09-22 |
|
| PRODUCT-001 | 全栈开发(待指定) | 发布规则与人工样例可审阅;非法字段/操作符阻止发布;fail优先于unknown,匹配指标不作为审批概率。 | 未验收;无真实业务验收证据;本次v2契约已补,新增业务尚未开始,原测试不覆盖v2 | 2026-09-23 |
|
||||||
| PRODUCT-002 | 全栈开发(待指定) | 下架/过期/硬条件不符不推荐;缺失条件不当已满足;同数据同版本结果稳定。 | 未验收;无真实业务验收证据 | 2026-09-22 |
|
| PRODUCT-002 | 全栈开发(待指定) | 同输入/快照结果稳定;未知不当满足,配置错误不归咎用户;需求不拆额、不改期;未运行与零候选区分。 | 未验收;无真实业务验收证据;本次v2契约已补,新增业务尚未开始,原测试不覆盖v2 | 2026-09-23 |
|
||||||
| PRODUCT-003 | 全栈开发(待指定) | 上架完整校验;版本递增;公共详情无用户分数;真实产品有运营确认依据。 | 未验收;无真实业务验收证据 | 2026-09-22 |
|
| PRODUCT-003 | 全栈开发(待指定) | 产品用途/地区/额度/期限有真实依据;规则可配置并启停,版本变化留依据;公共详情无个人分数。 | 未验收;无真实业务验收证据;本次v2契约已补,新增业务尚未开始,原测试不覆盖v2 | 2026-09-23 |
|
||||||
| PRODUCT-004 | 全栈开发(待指定) | 按assessmentId显示;参数历史快照稳定;当前不可用产品清晰提示;无匹配有说明。 | 未验收;无真实业务验收证据 | 2026-09-22 |
|
| PRODUCT-004 | 全栈开发(待指定) | 每条解释可追溯且不凑三正三反;当前产品变化不改历史;跨账户拒绝;unavailable/零候选/failed不混淆。 | 未验收;无真实业务验收证据;本次v2契约已补,新增业务尚未开始,原测试不覆盖v2 | 2026-09-23 |
|
||||||
|
|
||||||
直接阻塞:PRODUCT-001 → DEC-MATCH;PRODUCT-003 → DEC-PRODUCT。
|
直接阻塞:PRODUCT-001 → DEC-MATCH;PRODUCT-003 → DEC-PRODUCT。
|
||||||
|
|
||||||
@@ -285,19 +362,19 @@ P0 表示核心流程、安全、首发外部能力或发布门槛;P1 表示
|
|||||||
|
|
||||||
| 任务编号 | 任务 | 范围 | 优先级 | 直接依赖 | 预计人日 | 状态 |
|
| 任务编号 | 任务 | 范围 | 优先级 | 直接依赖 | 预计人日 | 状态 |
|
||||||
| --- | --- | --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- | --- | --- |
|
||||||
| REPORT-001 | DeepSeek适配与配额 | 配置模型标识、服务端密钥读取、超时、结构化输出校验、配额预留和消耗记录。 | P0 | BASE-002 | 1.5–2 | 阻塞 |
|
| REPORT-001 | DeepSeek适配与配额 | 配置模型标识、服务端密钥读取、超时、结构化输出校验、配额预留和消耗记录。 | P0 | BASE-002、EXT-AI、DEC-AI | 1.5–2 | 待验收 |
|
||||||
| REPORT-002 | 异步任务与补偿 | think-queue/Redis业务Job、提交后派发、漏投扫描、租约/超时、重试和任务隔离。 | P0 | ASSESS-003、REPORT-001 | 2.5–4 | 未开始 |
|
| REPORT-002 | 异步任务与补偿 | 保留Redis派发、补投、租约和taskId隔离;沿用提交时input_snapshot,一次性补入execution_snapshot并在重试时复用,支持跨部署v1/v2任务。 | P0 | ASSESS-003、REPORT-001 | 2.5–4 | 开发中 |
|
||||||
| REPORT-003 | 报告生成与持久化 | 冻结已授权查询结果和产品匹配快照,调用AI解释,原子写report_data/match_result及状态。 | P0 | REPORT-002、DATA-004、PRODUCT-004 | 2–3 | 阻塞 |
|
| REPORT-003 | 报告生成与持久化 | 保留v1自填报告;补v2需求、完整度/准备等级、规则证据及定性AI区块;版本快照和正式结果原子落库。 | P0 | REPORT-002、DATA-004、PRODUCT-004、DEC-MATCH、DEC-READINESS | 2–3 | 阻塞 |
|
||||||
| REPORT-004 | 分析报告历史联调 | 状态轮询、刷新恢复、失败重试、真实雷达维度、历史详情和模型版本展示。 | P0 | REPORT-003、ASSESS-004 | 2–3 | 未开始 |
|
| REPORT-004 | 分析报告历史联调 | 保留轮询/重试和v1历史;接v2需求、完整度、准备等级、三态匹配及证据解释,不显示无规则分数/雷达。 | P0 | REPORT-003、ASSESS-004 | 2–3 | 开发中 |
|
||||||
|
|
||||||
| 任务编号 | 负责人 | 验收条件 | 证据 | 更新时间 |
|
| 任务编号 | 负责人 | 验收条件 | 证据 | 更新时间 |
|
||||||
| --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- |
|
||||||
| REPORT-001 | 全栈开发(待指定) | 模型可配置;密钥不下发;失败与使用量可追踪;数值事实不被模型改写。 | 未验收;无真实业务验收证据 | 2026-09-22 |
|
| REPORT-001 | 全栈开发(待指定) | 模型可配置;密钥不下发;失败与使用量可追踪;数值事实不被模型改写。 | 两个指定模型真实连通;Flash个人/企业报告通过;结构/隐私/用量/开发额度已实现,商业预算待定 | 2026-09-23 |
|
||||||
| REPORT-002 | 全栈开发(待指定) | 派发失败/worker重启可恢复;旧taskId不能落结果;重复消费不重复完成。 | 未验收;无真实业务验收证据 | 2026-09-22 |
|
| REPORT-002 | 全栈开发(待指定) | 漏投/中断可恢复或明确失败;旧任务不可回写;输入/目标结果/策略/产品快照重试不隐式变化。 | 54项报告检查覆盖目标版本冻结、超时重试快照不变、旧v2输入仍v1、损坏快照先于模型失败;产品快照/生产守护未完成,旧29项任务证据保留 | 2026-09-24 |
|
||||||
| REPORT-003 | 全栈开发(待指定) | 结构无效不得记成功;评分/证据来源明确;产品改动不改历史;部分资料缺失有标注。 | 未验收;无真实业务验收证据 | 2026-09-22 |
|
| REPORT-003 | 全栈开发(待指定) | 完整度按发布字段集复算,等级有依据且非审批;null rank/score按状态验证;格式错误不成功,产品/规则更新不改旧报告。 | v2需求/完整度/准备状态/依据及定性区块已落库;54项专项及15项HTTP通过;本轮假模型,真实产品/外部查询和v2语义验收仍待完成 | 2026-09-24 |
|
||||||
| REPORT-004 | 全栈开发(待指定) | 个人/企业各完成一条真实闭环;分析失败不会跳成功报告;再次评估生成独立记录。 | 未验收;无真实业务验收证据 | 2026-09-22 |
|
| REPORT-004 | 全栈开发(待指定) | 个人/企业真实链路按版本展示;未发布无分或等级,100%不等于授信;旧报告不重算;新复评新assessmentId。 | Chromium手机/桌面个人79%、企业100%、档案→报告→刷新、未发布无分和旧v1不重算通过;Web/Android编译通过;微信内/Safari/真机待验收 | 2026-09-24 |
|
||||||
|
|
||||||
直接阻塞:REPORT-001 → EXT-AI、DEC-AI;REPORT-003 → DEC-MATCH。
|
剩余依赖:REPORT-001商业预算/生产额度待DEC-AI;REPORT-003完整报告仍依赖DATA、PRODUCT、DEC-MATCH;DEC-READINESS首版开发策略已发布,人工业务验收待完成。模型真实权限已验证。
|
||||||
|
|
||||||
### CHAT|AI助手
|
### CHAT|AI助手
|
||||||
|
|
||||||
@@ -365,7 +442,7 @@ P0 表示核心流程、安全、首发外部能力或发布门槛;P1 表示
|
|||||||
|
|
||||||
| 任务编号 | 任务 | 范围 | 优先级 | 直接依赖 | 预计人日 | 状态 |
|
| 任务编号 | 任务 | 范围 | 优先级 | 直接依赖 | 预计人日 | 状态 |
|
||||||
| --- | --- | --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- | --- | --- |
|
||||||
| POINT-001 | 积分规则定稿 | 确认签到、资料完成、邀请奖励的条件/次数/额度、业务时区和权益范围。 | P1 | BASE-001 | 0.5–1 | 阻塞 |
|
| POINT-001 | 积分规则定稿 | 确认签到、资料完成、邀请奖励的条件/次数/额度、业务时区和权益范围。 | P1 | BASE-001、DEC-POINT | 0.5–1 | 阻塞 |
|
||||||
| POINT-002 | 积分账户与签到 | 事务更新score与流水、业务唯一键、服务端日期签到、余额/流水查询。 | P1 | POINT-001、AUTH-002 | 1.5–2 | 未开始 |
|
| POINT-002 | 积分账户与签到 | 事务更新score与流水、业务唯一键、服务端日期签到、余额/流水查询。 | P1 | POINT-001、AUTH-002 | 1.5–2 | 未开始 |
|
||||||
| POINT-003 | 邀请码与邀请关系 | 生成唯一邀请码、首次有效绑定、防自邀、可信奖励事件与发放幂等。 | P1 | POINT-002 | 1–2 | 未开始 |
|
| POINT-003 | 邀请码与邀请关系 | 生成唯一邀请码、首次有效绑定、防自邀、可信奖励事件与发放幂等。 | P1 | POINT-002 | 1–2 | 未开始 |
|
||||||
| POINT-004 | 任务积分页面联调 | 真实余额、流水、签到状态、个人邀请码、任务状态和规则说明。 | P1 | POINT-003、ASSESS-004 | 1–2 | 未开始 |
|
| POINT-004 | 任务积分页面联调 | 真实余额、流水、签到状态、个人邀请码、任务状态和规则说明。 | P1 | POINT-003、ASSESS-004 | 1–2 | 未开始 |
|
||||||
@@ -385,9 +462,9 @@ P0 表示核心流程、安全、首发外部能力或发布门槛;P1 表示
|
|||||||
|
|
||||||
| 任务编号 | 任务 | 范围 | 优先级 | 直接依赖 | 预计人日 | 状态 |
|
| 任务编号 | 任务 | 范围 | 优先级 | 直接依赖 | 预计人日 | 状态 |
|
||||||
| --- | --- | --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- | --- | --- |
|
||||||
| SUPPORT-001 | 客服方案与运营准备 | 选定H5客服、身份签名、坐席/工作时间、离线策略、反馈状态及责任人。 | P0 | BASE-001 | 0.5–1 | 阻塞 |
|
| SUPPORT-001 | 客服方案与运营准备 | 选定H5客服、身份签名、坐席/工作时间、离线策略、反馈状态及责任人。 | P0 | BASE-001、EXT-CS | 0.5–1 | 阻塞 |
|
||||||
| SUPPORT-002 | 反馈提交与处理闭环 | 帮助页增加反馈表单/记录;附件权限、后台状态/回复、用户查看处理结果。 | P1 | AUTH-002、FILE-002 | 1–2 | 未开始 |
|
| SUPPORT-002 | 反馈提交与处理闭环 | 帮助页增加反馈表单/记录;附件权限、后台状态/回复、用户查看处理结果。 | P1 | AUTH-002、FILE-002 | 1–2 | 未开始 |
|
||||||
| SUPPORT-003 | H5人工客服接入 | 服务端签发访客身份、前端打开客服、在线/排队/离线、失败回退。 | P0 | SUPPORT-001、AUTH-002 | 1–2 | 阻塞 |
|
| SUPPORT-003 | H5人工客服接入 | 服务端签发访客身份、前端打开客服、在线/排队/离线、失败回退。 | P0 | SUPPORT-001、AUTH-002、EXT-CS | 1–2 | 阻塞 |
|
||||||
| SUPPORT-004 | 客服反馈联调 | 普通/微信内H5、账户切换、关闭重开、无坐席、SDK失败和消息记录。 | P0 | SUPPORT-002、SUPPORT-003、CONTENT-004 | 0.5–1 | 未开始 |
|
| SUPPORT-004 | 客服反馈联调 | 普通/微信内H5、账户切换、关闭重开、无坐席、SDK失败和消息记录。 | P0 | SUPPORT-002、SUPPORT-003、CONTENT-004 | 0.5–1 | 未开始 |
|
||||||
|
|
||||||
| 任务编号 | 负责人 | 验收条件 | 证据 | 更新时间 |
|
| 任务编号 | 负责人 | 验收条件 | 证据 | 更新时间 |
|
||||||
@@ -427,7 +504,7 @@ P0 表示核心流程、安全、首发外部能力或发布门槛;P1 表示
|
|||||||
| --- | --- | --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- | --- | --- |
|
||||||
| RELEASE-001 | 核心与权限回归 | 完整个人/企业流程、跨用户攻击、金额边界、幂等、产品变更及并发用例。 | P0 | REPORT-004、LOGIN-004、AUTH-004、ADMIN-004 | 2–3 | 未开始 |
|
| RELEASE-001 | 核心与权限回归 | 完整个人/企业流程、跨用户攻击、金额边界、幂等、产品变更及并发用例。 | P0 | REPORT-004、LOGIN-004、AUTH-004、ADMIN-004 | 2–3 | 未开始 |
|
||||||
| RELEASE-002 | H5与第三方真实验收 | 普通手机/桌面/微信内浏览器,键盘/上传/回跳/断网/弱网及全部渠道成功失败验证。 | P0 | RELEASE-001 | 2–3 | 未开始 |
|
| RELEASE-002 | H5与第三方真实验收 | 普通手机/桌面/微信内浏览器,键盘/上传/回跳/断网/弱网及全部渠道成功失败验证。 | P0 | RELEASE-001 | 2–3 | 未开始 |
|
||||||
| RELEASE-003 | 部署与恢复演练 | 分环境配置、增量迁移预检、HTTPS、队列守护/告警、备份恢复、发布回滚手册。 | P0 | RELEASE-001、ADMIN-003 | 1.5–2.5 | 阻塞 |
|
| RELEASE-003 | 部署与恢复演练 | 分环境配置、增量迁移预检、HTTPS、队列守护/告警、备份恢复、发布回滚手册。 | P0 | RELEASE-001、ADMIN-003、EXT-DEPLOY | 1.5–2.5 | 阻塞 |
|
||||||
| RELEASE-004 | 首发验收与发布记录 | 核对范围/外部依赖/验收证据,正式发布检查,发布后冒烟及回退条件。 | P0 | RELEASE-002、RELEASE-003 | 1.5–2.5 | 未开始 |
|
| RELEASE-004 | 首发验收与发布记录 | 核对范围/外部依赖/验收证据,正式发布检查,发布后冒烟及回退条件。 | P0 | RELEASE-002、RELEASE-003 | 1.5–2.5 | 未开始 |
|
||||||
|
|
||||||
| 任务编号 | 负责人 | 验收条件 | 证据 | 更新时间 |
|
| 任务编号 | 负责人 | 验收条件 | 证据 | 更新时间 |
|
||||||
@@ -439,7 +516,7 @@ P0 表示核心流程、安全、首发外部能力或发布门槛;P1 表示
|
|||||||
|
|
||||||
直接阻塞:RELEASE-003 → EXT-DEPLOY。
|
直接阻塞:RELEASE-003 → EXT-DEPLOY。
|
||||||
|
|
||||||
任务状态计数(仅用于核对台账):未开始 40 项;开发中 5 项;待联调 0 项;待验收 0 项;已完成 0 项;阻塞 15 项。这些计数不换算为项目完成率。
|
任务状态计数(仅用于核对台账):未开始 34 项;开发中 11 项;待联调 0 项;待验收 1 项;已完成 0 项;阻塞 14 项。这些计数不换算为项目完成率。
|
||||||
|
|
||||||
## 5. 外部依赖、业务决策与阻塞传播
|
## 5. 外部依赖、业务决策与阻塞传播
|
||||||
|
|
||||||
@@ -454,11 +531,11 @@ P0 表示核心流程、安全、首发外部能力或发布门槛;P1 表示
|
|||||||
| EXT-ONE | 服务商及 H5 能力待定 | 明确浏览器/运营商/网络限制、合法的服务端验证接口、测试环境、SDK 和降级策略 | LOGIN-001、LOGIN-003 | 业务与开发联合选型;M1 接入前解除 |
|
| EXT-ONE | 服务商及 H5 能力待定 | 明确浏览器/运营商/网络限制、合法的服务端验证接口、测试环境、SDK 和降级策略 | LOGIN-001、LOGIN-003 | 业务与开发联合选型;M1 接入前解除 |
|
||||||
| EXT-STORAGE | 私有存储服务待定 | 私有桶或等价私有存储、权限策略、服务端访问配置、签名链接/鉴权读取能力、容量和生命周期方案 | FILE-001 | 开发提出方案,业务确认预算与保留边界;M1 开发前解除 |
|
| EXT-STORAGE | 私有存储服务待定 | 私有桶或等价私有存储、权限策略、服务端访问配置、签名链接/鉴权读取能力、容量和生命周期方案 | FILE-001 | 开发提出方案,业务确认预算与保留边界;M1 开发前解除 |
|
||||||
| EXT-DATA | 数据渠道待定 | 可开通数据产品、个人/企业覆盖、正式授权流程、样例与字段字典、验签/回调/对账、计费及测试权限 | DATA-001 | 业务确认可获得的产品及资质,开发验证接口;M0 发起,M2 前解除 |
|
| EXT-DATA | 数据渠道待定 | 可开通数据产品、个人/企业覆盖、正式授权流程、样例与字段字典、验签/回调/对账、计费及测试权限 | DATA-001 | 业务确认可获得的产品及资质,开发验证接口;M0 发起,M2 前解除 |
|
||||||
| EXT-AI | DeepSeek 已选定;具体模型/额度/测试权限待核验 | 确认可用模型标识、服务端测试配置、实际额度、返回格式、超时和费用核对入口;完成最小真实调用 | REPORT-001 | 开发验证能力,业务确认预算;M2 接入前解除 |
|
| EXT-AI | 腾讯云TokenHub;Pro/Flash真实权限、JSON和用量字段已验证;余额/预算待运营确认 | 确认可用模型标识、服务端测试配置、实际额度、返回格式、超时和费用核对入口;完成最小真实调用 | REPORT-001 | 开发验证能力,业务确认预算;M2 接入前解除 |
|
||||||
| EXT-CS | 客服服务商和坐席待定 | 支持普通/微信内 H5 的客服方案、用户身份签名、测试坐席、工作时间与离线接待责任人 | SUPPORT-001、SUPPORT-003 | 业务负责供应商与坐席,开发负责接入;M3 前解除 |
|
| EXT-CS | 客服服务商和坐席待定 | 支持普通/微信内 H5 的客服方案、用户身份签名、测试坐席、工作时间与离线接待责任人 | SUPPORT-001、SUPPORT-003 | 业务负责供应商与坐席,开发负责接入;M3 前解除 |
|
||||||
| EXT-DEPLOY | 部署目标及域名资源待定 | 测试/生产资源、域名/HTTPS、数据库/Redis、队列进程守护、日志监控、备份位置及恢复权限 | RELEASE-003 | 业务确认资源,开发落地运维;M0 收集,M4 演练前解除 |
|
| EXT-DEPLOY | 部署目标及域名资源待定 | 测试/生产资源、域名/HTTPS、数据库/Redis、队列进程守护、日志监控、备份位置及恢复权限 | RELEASE-003 | 业务确认资源,开发落地运维;M0 收集,M4 演练前解除 |
|
||||||
|
|
||||||
“DeepSeek 已选定”只表示供应商选择完成,不代表 EXT-AI 已解除。其他服务商未定不是本轮文档交付的阻塞,但确实阻塞对应真实接入任务及整体一期验收。
|
EXT-AI已具备真实调用证据,尚不代表商业预算与生产容量验收完成。其他服务商未定不是本轮文档交付的阻塞,但确实阻塞对应真实接入任务及整体一期验收。
|
||||||
|
|
||||||
### 5.2 业务规则决策
|
### 5.2 业务规则决策
|
||||||
|
|
||||||
@@ -466,9 +543,11 @@ P0 表示核心流程、安全、首发外部能力或发布门槛;P1 表示
|
|||||||
| --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- |
|
||||||
| DEC-PRIVACY | 待确认 | 资料用途、可选/必选项、协议正式全文与版本、保存期限、撤销后处理及对外字段范围 | 协议与资料生命周期清单;FILE-001、FILE-003 | M1 私有资料/授权实现前 |
|
| DEC-PRIVACY | 待确认 | 资料用途、可选/必选项、协议正式全文与版本、保存期限、撤销后处理及对外字段范围 | 协议与资料生命周期清单;FILE-001、FILE-003 | M1 私有资料/授权实现前 |
|
||||||
| DEC-QUERY | 待确认 | 首发查询项目、个人/企业适用条件、查询失败/拒绝后的自填路径、重新查询触发和费用归属 | 数据产品与页面授权逐项映射;DATA-001 | M2 渠道实现前 |
|
| DEC-QUERY | 待确认 | 首发查询项目、个人/企业适用条件、查询失败/拒绝后的自填路径、重新查询触发和费用归属 | 数据产品与页面授权逐项映射;DATA-001 | M2 渠道实现前 |
|
||||||
| DEC-MATCH | 待确认 | 硬准入、未知值、评分维度/权重、等级阈值、排序同分规则、报告雷达维度和人工金标准样例 | 版本化规则及人工确认样例;PRODUCT-001、REPORT-003 | M2 匹配引擎和正式报告实现前 |
|
| DEC-DEMAND | 已确认并发布 | 用户授权采用个人/企业用途、other说明及v2必填;新建最低2 | assessment.php用途版本、76项专项与H5证据;ASSESS-001/003 | 2026-09-24复核 |
|
||||||
|
| DEC-READINESS | 首版开发策略已发布 | 用户继续下一步授权采用既有等权字段集、条件分母和资料准备状态;不代表授信 | readiness.php、54项专项和H5;人工业务样例待验收 | 2026-09-24实施;生产发布前验收 |
|
||||||
|
| DEC-MATCH | 待确认 | 硬准入三态、来源要求、单产品需求覆盖、用途规则、排序及可选匹配指标、解释证据;资料等级另见DEC-READINESS | 版本化规则及人工确认样例;PRODUCT-001、REPORT-003 | M2 匹配引擎和正式报告实现前 |
|
||||||
| DEC-PRODUCT | 待确认 | 首发真实产品、地区与对象、额度/期限/利率/准入条件、有效期、发布责任人、申请准备说明 | 可录入的真实产品资料及运营确认记录;PRODUCT-003 | 产品发布/匹配真实验收前 |
|
| DEC-PRODUCT | 待确认 | 首发真实产品、地区与对象、额度/期限/利率/准入条件、有效期、发布责任人、申请准备说明 | 可录入的真实产品资料及运营确认记录;PRODUCT-003 | 产品发布/匹配真实验收前 |
|
||||||
| DEC-AI | 待确认 | 具体模型、单用户/单评估额度、重试上限、超时、输入最小化、月预算及超额处理 | AI 配置和额度规则;REPORT-001 | DeepSeek 接入和压测前 |
|
| DEC-AI | 模型已确认,其余待确认 | 单用户/单评估额度、重试上限、超时、输入最小化、月预算及超额处理 | AI 配置和额度规则;REPORT-001 | DeepSeek 接入和压测前 |
|
||||||
| DEC-POINT | 待确认 | 签到/资料/邀请奖励数额、次数、可信达成条件、业务时区、有效期与权益范围 | 版本化奖励规则及反例;POINT-001 | M3 积分实现前 |
|
| DEC-POINT | 待确认 | 签到/资料/邀请奖励数额、次数、可信达成条件、业务时区、有效期与权益范围 | 版本化奖励规则及反例;POINT-001 | M3 积分实现前 |
|
||||||
|
|
||||||
上述决定没有在本台账中擅自填入分数、积分数额或第三方费用。它们本身属于对应已有任务的工作,不新增第 61 项开发任务。涉及新增表或字段时,先在相应任务记录增量方案、回填与恢复步骤,再实施迁移。
|
上述决定没有在本台账中擅自填入分数、积分数额或第三方费用。它们本身属于对应已有任务的工作,不新增第 61 项开发任务。涉及新增表或字段时,先在相应任务记录增量方案、回填与恢复步骤,再实施迁移。
|
||||||
@@ -485,7 +564,7 @@ P0 表示核心流程、安全、首发外部能力或发布门槛;P1 表示
|
|||||||
|
|
||||||
| 日期 | 阻塞编号 | 原因/当前进展 | 责任人 | 预计解除日 | 解除依据/证据 | 受影响任务与计划调整 |
|
| 日期 | 阻塞编号 | 原因/当前进展 | 责任人 | 预计解除日 | 解除依据/证据 | 受影响任务与计划调整 |
|
||||||
| --- | --- | --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- | --- | --- |
|
||||||
| 2026-09-22 | EXT-AI | 已选 DeepSeek,模型、预算、额度与测试接入待核验 | 待指定 | 待确认 | 用户已确认供应商;无真实调用证据 | REPORT-001 保持阻塞,其他依赖按台账推进 |
|
| 2026-09-22 | EXT-AI | 指定模型与本地密钥配置已就绪,预算、额度与真实调用待核验 | 待指定 | 待确认 | 用户已确认供应商;无真实调用证据 | REPORT-001配置开发已启动,DEC-AI及真实调用验收仍待完成 |
|
||||||
| 2026-09-22 | 其余 EXT / 全部 DEC | 服务资源或正式业务口径待补齐 | 待指定 | 待确认 | 本轮计划基线,无已解除证据 | 逐项维护,不合并视为可跳过 |
|
| 2026-09-22 | 其余 EXT / 全部 DEC | 服务资源或正式业务口径待补齐 | 待指定 | 待确认 | 本轮计划基线,无已解除证据 | 逐项维护,不合并视为可跳过 |
|
||||||
|
|
||||||
## 6. 测试与验收台账
|
## 6. 测试与验收台账
|
||||||
@@ -505,7 +584,7 @@ P0 表示核心流程、安全、首发外部能力或发布门槛;P1 表示
|
|||||||
|
|
||||||
### 6.2 一期验收矩阵
|
### 6.2 一期验收矩阵
|
||||||
|
|
||||||
本轮浏览器证据与复核范围:使用Playwright CLI(`Browser plugin not available`),目标为真实密码登录→首页→底栏“我的”→确认退出→登录页,另验证A/B草稿隔离及短信未接入提示。页面标题“AI融资”与路由正确、非空、无框架错误遮罩;桌面及手机视口无本轮新增遮挡。正常登录/退出控制台无错误或警告;短信场景唯一错误为预期的HTTP 503资源日志。早期脚本直接改hash使弹框检查失效,改用实际底栏导航后完整退出流程通过,未修改框架。
|
前轮(草稿取消前)的浏览器证据与复核范围:使用Playwright CLI(`Browser plugin not available`),目标为真实密码登录→首页→底栏“我的”→确认退出→登录页,另验证A/B草稿隔离及短信未接入提示。页面标题“AI融资”与路由正确、非空、无框架错误遮罩;桌面及手机视口无本轮新增遮挡。正常登录/退出控制台无错误或警告;短信场景唯一错误为预期的HTTP 503资源日志。早期脚本直接改hash使弹框检查失效,改用实际底栏导航后完整退出流程通过,未修改框架。
|
||||||
|
|
||||||
截图:[登录后个人中心](<C:/Users/JUN/AppData/Local/Temp/airz-redis-qa/profile-mobile.png>)、[切换账户后的空白草稿](<C:/Users/JUN/AppData/Local/Temp/airz-redis-qa/account-isolation-mobile.png>)、[短信未配置手机提示](<C:/Users/JUN/AppData/Local/Temp/airz-redis-qa/sms-unavailable-mobile.png>)、[桌面提示](<C:/Users/JUN/AppData/Local/Temp/airz-redis-qa/sms-unavailable-desktop.png>)。位于本机临时目录,清理后可能失效,正式验收应另行归档。两条临时合成账户及其Redis会话已清理;复核数据库25表、Token表不存在。未覆盖微信内置浏览器、Safari、真机、正式发行、真实短信/AI或产品链路。接口复核命令为`php tests/api_session.php`,驱动为`php tests/token_redis.php`;前端编译命令见开发文档8.2。
|
截图:[登录后个人中心](<C:/Users/JUN/AppData/Local/Temp/airz-redis-qa/profile-mobile.png>)、[切换账户后的空白草稿](<C:/Users/JUN/AppData/Local/Temp/airz-redis-qa/account-isolation-mobile.png>)、[短信未配置手机提示](<C:/Users/JUN/AppData/Local/Temp/airz-redis-qa/sms-unavailable-mobile.png>)、[桌面提示](<C:/Users/JUN/AppData/Local/Temp/airz-redis-qa/sms-unavailable-desktop.png>)。位于本机临时目录,清理后可能失效,正式验收应另行归档。两条临时合成账户及其Redis会话已清理;复核数据库25表、Token表不存在。未覆盖微信内置浏览器、Safari、真机、正式发行、真实短信/AI或产品链路。接口复核命令为`php tests/api_session.php`,驱动为`php tests/token_redis.php`;前端编译命令见开发文档8.2。
|
||||||
|
|
||||||
@@ -514,7 +593,7 @@ P0 表示核心流程、安全、首发外部能力或发布门槛;P1 表示
|
|||||||
| 场景 | 关联任务 | 必须验证的结果 | 日期 / 环境 | 状态 | 证据 / 验收人 |
|
| 场景 | 关联任务 | 必须验证的结果 | 日期 / 环境 | 状态 | 证据 / 验收人 |
|
||||||
| --- | --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- | --- |
|
||||||
| 请求与 Token | BASE-002、AUTH-002 | HTTP 200 业务错误不能被当成功;过期/撤销 Token 拒绝;错误可定位 requestId | 待记录 / 测试 API + H5 | 未执行 | 待补充 / 待指定 |
|
| 请求与 Token | BASE-002、AUTH-002 | HTTP 200 业务错误不能被当成功;过期/撤销 Token 拒绝;错误可定位 requestId | 待记录 / 测试 API + H5 | 未执行 | 待补充 / 待指定 |
|
||||||
| 账户隔离 | BASE-003、ASSESS-002、CHAT-001、MSG-002 | A/B 用户切换不串草稿、消息、聊天、资料和历史;跨用户 ID 读写失败 | 待记录 / 至少两个测试账户 | 未执行 | 待补充 / 待指定 |
|
| 账户隔离 | BASE-003、ASSESS-002、CHAT-001、MSG-002 | A/B 用户切换不串临时填写内容、消息、聊天、资料和历史;跨用户 ID 读写失败 | 待记录 / 至少两个测试账户 | 未执行 | 待补充 / 待指定 |
|
||||||
| 短信与登录 | AUTH-001、AUTH-002 | 真短信成功/失败、用途隔离、频控、过期和核销;无假 Token | 待记录 / 真实短信测试权限 | 未执行 | 待补充 / 待指定 |
|
| 短信与登录 | AUTH-001、AUTH-002 | 真短信成功/失败、用途隔离、频控、过期和核销;无假 Token | 待记录 / 真实短信测试权限 | 未执行 | 待补充 / 待指定 |
|
||||||
| 改密与换绑 | AUTH-004 | 原/新手机双挑战,号码占用拒绝,事务一致,修改后全部旧会话失效 | 待记录 / 测试 API + 双账户 | 未执行 | 待补充 / 待指定 |
|
| 改密与换绑 | AUTH-004 | 原/新手机双挑战,号码占用拒绝,事务一致,修改后全部旧会话失效 | 待记录 / 测试 API + 双账户 | 未执行 | 待补充 / 待指定 |
|
||||||
| 微信授权 | LOGIN-002、LOGIN-004 | 获批 H5 场景真实成功;取消/state 错误/code 重放/已有账号冲突安全处理 | 待记录 / 微信内与支持的普通 H5 | 未执行 | 待补充 / 待指定 |
|
| 微信授权 | LOGIN-002、LOGIN-004 | 获批 H5 场景真实成功;取消/state 错误/code 重放/已有账号冲突安全处理 | 待记录 / 微信内与支持的普通 H5 | 未执行 | 待补充 / 待指定 |
|
||||||
@@ -555,6 +634,12 @@ P0 表示核心流程、安全、首发外部能力或发布门槛;P1 表示
|
|||||||
|
|
||||||
| 日期 | 更新内容 | 任务状态变化 | 验证范围 | 下一步 |
|
| 日期 | 更新内容 | 任务状态变化 | 验证范围 | 下一步 |
|
||||||
| --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- |
|
||||||
|
| 2026-09-24 | 实施等权完整度、准备状态、执行策略快照及报告v2自填支线,无迁移 | DEC-READINESS开发版已发布;REPORT-002/004开发中,REPORT-003完整范围仍阻塞 | 54项报告+76项需求+41项基础+15项HTTP;H5手机/桌面及Web/Android编译 | 验收v2真实模型语义和策略样例;待DEC-MATCH与真实产品后接匹配引擎 |
|
||||||
|
| 2026-09-24 | 完成需求输入v2、字典、迁移、正式提交与历史回显,保留v1报告 | DEC-DEMAND已发布;ASSESS-002待验收;其余相关任务保留开发中/阻塞 | 76项专项+41/29/15回归;H5手机/桌面、字典恢复与无草稿;Web/Android编译 | 确认DEC-READINESS字段集和等级,再实现执行策略快照与报告v2;真实产品待准备 |
|
||||||
|
| 2026-09-23 | 补需求字段、完整度/准备等级与匹配解释契约,明确v1/v2/策略快照及迁移边界;同步主文档 | ASSESS-002/003、REPORT-002整体改开发中;新增DEC-DEMAND/DEC-READINESS待确认;本轮仅文档 | 字段/枚举/JSON样例、编号/基线人日/依赖及文档链接静态核对;未执行业务测试或迁移 | 先确认用途/必填与准备策略,再按ASSESS→REPORT/PRODUCT实施,重估新增工作量 |
|
||||||
|
| 2026-09-23 | DeepSeek适配、持久任务、幂等重试、自填报告;本地启用并启动worker | ASSESS-003、REPORT-001/002待验收,REPORT-003完整范围仍阻塞 | 29项任务+15项HTTP;真实个人报告、企业H5重试/报告;Android编译 | 决定匹配评分口径并准备真实产品;补私有资料/正式授权及运营排障 |
|
||||||
|
| 2026-09-23 | 正式提交/历史列表/详情/状态;严格路由、金额与归属校验、请求幂等、取消假分析 | ASSESS-002待验收;ASSESS-003/004、REPORT-004开发中 | 54项评估+41项基础+14项资料;H5个人/企业/断网重试/23条分页;Android编译 | 下一步接DeepSeek适配、可靠任务派发及重试,完善真实报告;短信/产品资料待准备 |
|
||||||
|
| 2026-09-22 | 取消评估草稿;账户资料/头像持久化、缓存清理;DeepSeek配置记录 | AUTH-003、ASSESS-001、REPORT-001开发中;ASSESS-002范围改正式历史 | 14项资料HTTP、41项基础、16项会话;H5昵称/头像与不保存填写内容;Android编译 | 下一步开发正式评估提交/历史及DeepSeek适配,等待真实产品与短信 |
|
||||||
| 2026-09-22 | Redis切换与M0/账户基础启动;Token表备份删除,真实密码会话与缓存隔离 | BASE-001/002/003、AUTH-002/004改开发中,外部依赖保留阻塞 | 41项基础+14项Redis+16项HTTP,Web/Android编译,H5退出/隔离/短信失败 | 继续完善BASE-001/003及资料API,供应商未就绪不阻塞内部开发 |
|
| 2026-09-22 | Redis切换与M0/账户基础启动;Token表备份删除,真实密码会话与缓存隔离 | BASE-001/002/003、AUTH-002/004改开发中,外部依赖保留阻塞 | 41项基础+14项Redis+16项HTTP,Web/Android编译,H5退出/隔离/短信失败 | 继续完善BASE-001/003及资料API,供应商未就绪不阻塞内部开发 |
|
||||||
| 2026-09-22 | 建立开发文档和进度基线;登记 15 模块、60 任务、25 页映射及外部/业务阻塞 | 业务任务按基线记录;本次没有把文档编写计为业务完成 | 文档任务编号、估算、页面范围与相互引用静态核对;无业务测试或迁移 | 从 BASE-001 开始,M0 同步推进选型、资源和业务规则准备 |
|
| 2026-09-22 | 建立开发文档和进度基线;登记 15 模块、60 任务、25 页映射及外部/业务阻塞 | 业务任务按基线记录;本次没有把文档编写计为业务完成 | 文档任务编号、估算、页面范围与相互引用静态核对;无业务测试或迁移 | 从 BASE-001 开始,M0 同步推进选型、资源和业务规则准备 |
|
||||||
|
|
||||||
@@ -567,3 +652,99 @@ P0 表示核心流程、安全、首发外部能力或发布门槛;P1 表示
|
|||||||
5. 外部前置就绪后推进 AUTH/LOGIN/FILE,再完成 ASSESS→DATA/PRODUCT→REPORT 核心闭环,随后按 M3、M4 验收全部首发功能。
|
5. 外部前置就绪后推进 AUTH/LOGIN/FILE,再完成 ASSESS→DATA/PRODUCT→REPORT 核心闭环,随后按 M3、M4 验收全部首发功能。
|
||||||
|
|
||||||
每次完成开发活动,追加一行更新日志并同步相应任务的证据、状态和日期。需要调整一期范围或工时必须在两份文档中一起记录;第三方未接通时保留阻塞,不以模拟替代验收。
|
每次完成开发活动,追加一行更新日志并同步相应任务的证据、状态和日期。需要调整一期范围或工时必须在两份文档中一起记录;第三方未接通时保留阻塞,不以模拟替代验收。
|
||||||
|
|
||||||
|
|
||||||
|
### 2026-09-22 本轮最终复核:无草稿与账户资料
|
||||||
|
|
||||||
|
环境:本地 `http://localhost:5175/`,Chromium,390×844及1280×900。Browser plugin not available,沿用Playwright CLI;本轮没有安装依赖。
|
||||||
|
|
||||||
|
| 检查 | 结果与边界 |
|
||||||
|
| --- | --- |
|
||||||
|
| 页面及渲染 | 标题AI融资、评估填写路由正确;非空、无框架错误遮罩;手机/桌面截图检查通过 |
|
||||||
|
| 控制台 | 最终会话0错误、0警告 |
|
||||||
|
| 无草稿流程 | 真实登录→融资档案→开始个人评估→填写→确认→编辑;返回后输入保留,刷新后清空,无草稿保存按钮或缓存键 |
|
||||||
|
| 账户资料 | 昵称与真实头像上传/保存/刷新回显已验证;默认头像恢复、附件跨用户拒绝由HTTP测试覆盖 |
|
||||||
|
| 后端检查 | 14项资料HTTP、41项基础、16项会话回归通过;本轮7个相关PHP文件语法检查通过 |
|
||||||
|
| 编译 | 18:02 Android本地编译成功,Web可运行;不代表真机或正式发行验收 |
|
||||||
|
| 数据清理 | 两个UI合成账户、头像附件及Redis会话已清理;本地剩1个原有用户、25张表,无fa_user_token及草稿专用字段 |
|
||||||
|
| 范围限制 | 正式提交/历史接口待开发;DeepSeek仅配置、未真实请求;未验证微信内置浏览器、Safari及真机 |
|
||||||
|
|
||||||
|
证据:[手机无草稿页面](<C:/Users/JUN/AppData/Local/Temp/airz-draft-qa/no-drafts-final-mobile.png>)、[桌面无草稿页面](<C:/Users/JUN/AppData/Local/Temp/airz-draft-qa/no-drafts-final-desktop.png>)、[头像持久化](<C:/Users/JUN/AppData/Local/Temp/airz-draft-qa/profile-persisted-mobile.png>)。截图位于临时目录,正式验收时需归档。
|
||||||
|
|
||||||
|
下一步按无草稿范围实现 `POST /assessments` 全量校验、提交幂等及事务建记录,随后接已提交历史与DeepSeek适配。真实产品与短信仍为外部依赖;不能以固定报告或模拟短信替代验收。
|
||||||
|
|
||||||
|
### 2026-09-23 本轮验证记录:正式提交与历史查询
|
||||||
|
|
||||||
|
环境:PHP 7.4.3开发服务127.0.0.1:80、本地MySQL/Redis;HBuilderX 5.26 Web http://localhost:5173/,Chromium 390×844及1280×900。Browser plugin not available,使用现有Playwright CLI,未安装依赖。
|
||||||
|
|
||||||
|
| 检查 | 证据与结果 |
|
||||||
|
| --- | --- |
|
||||||
|
| API及数据库 | `php tests/api_assessments.php` 54项通过:个人/企业、金额边界/精度/类型、零与null、加密/脱敏、越权、非法字段无落库、重复请求/冲突、两独立PHP进程竞争同一键、游标分页/筛选、软删除/内部draft排除、状态变更后原回执稳定 |
|
||||||
|
| 既有回归 | `php tests/finance.php` 41项通过;`php tests/api_profile.php` 14项通过,草稿断言改为POST /assessments/drafts不存在 |
|
||||||
|
| 真实H5 | 登录→空历史→个人提交0.800001万元→状态→详情刷新;企业18个月/120.123456万元回显;服务端已保存但客户端响应丢失后重试,复用同键且仅一条记录;23条记录分两页无重漏,最旧详情正确 |
|
||||||
|
| 不支持能力 | 勾选外部授权后提交被明确阻止;移除后可提交;分析暂未开放时显示真实原因,无计时成功或假报告跳转 |
|
||||||
|
| 页面检查 | 标题AI融资/路由正确、非空、无框架遮罩;手机/桌面截图可读;正常提交流程浏览器控制台0错误/0警告;故意断开响应及跨账户404为已预期负向场景 |
|
||||||
|
| 构建 | Android最后本地编译15:52:12成功;Web可运行。Web编译保留原有layout.uts类型提示,非本轮引入;无真机或正式发行结论 |
|
||||||
|
| 工具故障及修正 | 初始HBuilderX未启动,启动后继续;UTS异步递归声明限制已修复并重编译;ThinkPHP前缀路由误匹配已改完整匹配并回归 |
|
||||||
|
| 仍未验收 | 微信内H5、Safari、原生真机;私有上传/正式授权、DeepSeek真实调用、任务派发/重试、报告/匹配结果未接通 |
|
||||||
|
|
||||||
|
截图:[状态手机](<C:/Users/JUN/AppData/Local/Temp/airz-assessment-qa/status-mobile.png>)、[个人详情手机](<C:/Users/JUN/AppData/Local/Temp/airz-assessment-qa/detail-mobile.png>)、[企业详情手机](<C:/Users/JUN/AppData/Local/Temp/airz-assessment-qa/enterprise-mobile.png>)、[历史列表手机](<C:/Users/JUN/AppData/Local/Temp/airz-assessment-qa/history-mobile.png>)、[历史桌面](<C:/Users/JUN/AppData/Local/Temp/airz-assessment-qa/history-desktop.png>)。使用临时合成数据,不是真实客户资料,临时目录证据可能被清理。
|
||||||
|
|
||||||
|
本轮下一步:REPORT-001 DeepSeek适配与结构化输出检查;REPORT-002可靠派发及失败补偿,并补ASSESS-003请求级幂等重试。新接口开发前更新对应契约;产品匹配必须等待真实产品资料和规则口径,不用模型猜测硬准入。
|
||||||
|
|
||||||
|
最终清理:23条UI合成评估及关联会话、2个合成账户和Redis令牌已移除;复核本地25表、1个原有用户、0条评估记录。本轮无数据库迁移。补充H5验证:真实退出A并登录B,B历史为空,直接访问A详情显示不存在且无旧资料残留。测试脚本与合成凭据已清理,截图保留。
|
||||||
|
|
||||||
|
### 6.4 2026-09-23 本轮验证与后续
|
||||||
|
|
||||||
|
- 后端:`tests/analysis_tasks.php --worker-stopped` 29项通过(假模型,含两个独立PHP消费者);`tests/analysis_http.php --worker-stopped` 15项通过。先停止所有worker再运行,防止数据库恢复扫描拾取测试行;仅隔离Redis队列不足以隔离数据库扫描。
|
||||||
|
- 真实模型:两个指定版本连通成功;收紧输出规则后个人Flash报告 input582/output408 tokens、4438ms;企业浏览器真实worker报告成功。仅合成数据,不使用会员实际资料。测试调用次数不等同正式业务配额或费用账单。
|
||||||
|
- UI检查:页面身份、非空、无框架错误遮罩、控制台无相关运行错误、截图和交互均通过。金额 `1000000.01` 元准确显示 `100.000001 万元`。报告缺少产品规则时无固定分数或匹配入口。
|
||||||
|
- 浏览器证据:`C:/Users/JUN/AppData/Local/Temp/analysis-report-mobile.png`、`analysis-report-desktop.png`;本地临时文件,不提交仓库。Browser插件不可用,使用已安装Playwright CLI。Android HBuilderX 5.26编译通过。
|
||||||
|
- 下一步:PRODUCT-001/DEC-MATCH明确准入、未知值与评分排序口径,收集首批真实产品;内部可先做产品字段校验和版本快照,不生成假匹配。并行准备FILE私有资料/正式授权、ADMIN任务排障界面、短信选型和商业AI预算;一期仍未整体验收。
|
||||||
|
|
||||||
|
### 2026-09-23 需求契约后的下一步(历史计划,第1—2项已按0.5实施)
|
||||||
|
|
||||||
|
1. DEC-DEMAND:确认用途草案及必填口径;ASSESS-001实现字典与五列增量迁移,预检/备份后执行;本次未迁移。
|
||||||
|
2. ASSESS-003:先支持v1/v2及幂等重放,再由ASSESS-004接入需求录入/确认,由ASSESS-002补历史展示;按发布记录切换新建最低输入版本。
|
||||||
|
3. DEC-READINESS:发布等权字段集与资料准备等级;REPORT-002/003补冻结context及确定性完整度/等级,保持v1历史展示。
|
||||||
|
4. DEC-MATCH与真实产品准备完成后,PRODUCT实现三态规则、证据和快照,再接REPORT-004结果解释;此前不虚构匹配结果或评分。
|
||||||
|
5. 本专项验收用例见契约第8节,均待实施后执行。已通过的旧测试不覆盖新增字段、等级、用途和匹配解释。
|
||||||
|
|
||||||
|
### 6.5 2026-09-24 本轮最终验证:需求v2录入、提交与历史
|
||||||
|
|
||||||
|
环境:PHP 7.4.3、本地MySQL/Redis;HBuilderX 5.26,Web `http://localhost:5173/`,Chromium 390×844和1280×900。Browser plugin not available,使用已有Playwright CLI,未安装依赖。浏览器API转发至仅本机的隔离PHP服务,执行真实后端和数据库;该服务禁用模型,因此截图中“分析暂未开放”只代表隔离环境。本轮未修改真实模型开关或发起收费调用。
|
||||||
|
|
||||||
|
| 检查 | 日期与结果 | 边界/证据 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 需求API/数据 | 9月24日 `php tests/assessment_demand.php --worker-stopped` 76项通过 | 含个人/企业、金额/期限边界、非法输入无落库、other、全字段冲突、并发唯一记录、最低版本切换、旧用途/旧键、越权、损坏哈希503、假模型重试及旧回执 |
|
||||||
|
| 既有回归 | 9月23日 finance.php 41项、analysis_tasks.php 29项、analysis_http.php 15项通过 | 旧任务用例显式保留v1兼容配置;旧54项api_assessments.php未计本轮通过 |
|
||||||
|
| 手机个人流程 | 填写需求→确认→编辑期限→提交→状态→历史→刷新通过 | 12.345678万元仅转换一次为123456.78元;18个月/教育进修;切离other清说明 |
|
||||||
|
| 企业与旧历史 | 0.000001万元提交为0.01元,1个月/其他用途说明刷新保留;个人专属用途不出现 | v1金额/期限/用途均显示“当时未采集”,不补造值 |
|
||||||
|
| 故障与无草稿 | 用途接口503时提示“重新加载用途”,移除故障后选项恢复;刷新后收入/需求金额/期限/用途清空 | 缺少需求或other说明阻止下一步;只保留当前流程内存 |
|
||||||
|
| 页面与控制台 | 标题/路由正确、非空、无框架遮罩;手机/桌面可读 | 正常提交0错误/0警告;故障会话仅1条刻意模拟的503资源错误,恢复刷新无新增应用错误 |
|
||||||
|
| 编译 | 9月23日Web及Android本地编译成功,Android最终20:04:31 | 修复UTS函数声明顺序后通过;原layout.uts提示仍在;未验真机/微信内H5/Safari |
|
||||||
|
| 迁移与清理 | 9月23日已备份并完成9项增量,立即复跑预检0 DDL;9月24日测试账号/3条UI记录/令牌已清理,隔离服务已停止 | 最终25表、1个原会员、1条非测试评估及会话(processing)保留;无测试用户。9月24日预检因该运行中记录被保护规则拒绝,未执行DDL,不能记为本次复跑成功 |
|
||||||
|
|
||||||
|
测试断言曾将MySQL JSON对象键顺序当作回执内容,改为排序后严格比较类型和值,76项复跑通过。浏览器刷新后页面位于开发框架子页面,批量role读取工具报错;改用新DOM快照与截图核实空输入,没有据此修改业务代码。
|
||||||
|
|
||||||
|
证据:[需求填写](<C:/Users/JUN/AppData/Local/Temp/airz-demand-qa/demand-mobile.png>)、[确认](<C:/Users/JUN/AppData/Local/Temp/airz-demand-qa/confirm-mobile.png>)、[个人历史](<C:/Users/JUN/AppData/Local/Temp/airz-demand-qa/history-mobile.png>)、[桌面历史](<C:/Users/JUN/AppData/Local/Temp/airz-demand-qa/history-desktop.png>)、[企业历史](<C:/Users/JUN/AppData/Local/Temp/airz-demand-qa/enterprise-history-mobile.png>)、[用途失败](<C:/Users/JUN/AppData/Local/Temp/airz-demand-qa/dictionary-error-mobile.png>)、[用途恢复](<C:/Users/JUN/AppData/Local/Temp/airz-demand-qa/dictionary-recovered-mobile.png>)、[刷新清空](<C:/Users/JUN/AppData/Local/Temp/airz-demand-qa/no-draft-after-refresh.png>)。均为合成资料,本机临时证据待正式验收归档。
|
||||||
|
|
||||||
|
下一步:先确认DEC-READINESS中的字段集和准备等级,再补REPORT-002执行策略快照、REPORT-003确定性完整度/等级和报告v2;DEC-MATCH及真实产品就绪后开发匹配。本轮未启动模型worker处理现存非测试记录,也未修改其状态;需要分析时按开发文档启动 `php think analysis:work`。本轮需求子项待业务验收,不等于一期完成。
|
||||||
|
|
||||||
|
### 6.6 2026-09-24 本轮验收记录:完整度/准备状态及报告v2
|
||||||
|
|
||||||
|
环境:PHP 7.4.3、本地MySQL/Redis、HBuilderX 5.26;Chromium `http://localhost:5174/`(5173被占用,开发服务选择5174),390×844/1280×900。沿用frontend-testing-debugging与Playwright流程;Browser plugin not available,使用已有Playwright CLI,未安装依赖。
|
||||||
|
|
||||||
|
| 检查 | 结果与证据 | 边界 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 计算与任务 | `php tests/assessment_readiness.php --worker-stopped` 54项通过 | 含11/14=79、8/9=89、other分母、零值有效、invalid/一致性优先、未发布null、策略/快照损坏不调用模型、重试复用、旧目标版本、拒绝无依据/重复引用和审批承诺 |
|
||||||
|
| 需求与旧报告回归 | assessment_demand.php 76项、finance.php 41项、analysis_http.php 15项通过 | 本轮不运行含全库recover扫描的29项脚本,不修改原业务待处理任务 |
|
||||||
|
| 页面检查 | 标题与路由/记录编号一致,非空、无框架错误遮罩;最终0错误/0警告 | 开发框架刷新产生子页面,切换旧报告后完整刷新核对实际编号,不能仅凭地址判通过 |
|
||||||
|
| 真实接口H5 | 合成账号登录→报告79%/三项待补→历史→企业详情→报告100%→刷新;未发布无分、旧v1不添完整度均通过 | 报告由真实后端及数据库生成、假模型输出;不是DeepSeek真实语义或渠道验收 |
|
||||||
|
| 金额与来源 | 123456.78元显示12.345678万元、年营收100.000001万元、经营0个月;证据标签来自实际fact引用 | 姓名/手机/证件/企业名/城市职业自由文本/用途说明不发送模型 |
|
||||||
|
| 编译与语法 | Web和Android编译通过,相关12个PHP文件lint通过 | Web保留原layout.uts提示;最终Android编译11:47:22通过;无正式发行/真机结论 |
|
||||||
|
| 数据 | 无迁移、新表、原业务输入/历史报告重写;测试仅创建合成账户和记录 | 合成账号/4条UI报告及令牌已清理,测试服务已停;最终25表、1个原会员及其1条processing评估/会话保留,未启动真实模型worker |
|
||||||
|
|
||||||
|
截图:[个人需求与完整度](<C:/Users/JUN/AppData/Local/Temp/airz-readiness-qa/report-personal-mobile.png>)、[准备依据](<C:/Users/JUN/AppData/Local/Temp/airz-readiness-qa/readiness-mobile.png>)、[企业报告桌面](<C:/Users/JUN/AppData/Local/Temp/airz-readiness-qa/report-enterprise-desktop.png>)、[未发布规则](<C:/Users/JUN/AppData/Local/Temp/airz-readiness-qa/report-unpublished-desktop.png>)、[旧v1报告](<C:/Users/JUN/AppData/Local/Temp/airz-readiness-qa/report-legacy-desktop.png>)。截图均为合成资料/假模型,本地临时目录待正式归档。
|
||||||
|
|
||||||
|
下一步:报告v2真实模型和人工语义样例验收;DEC-MATCH确认准入三态、单产品需求覆盖及无评分排序,真实产品资料准备后实施PRODUCT-001/002/003/004。可先用合成样例测试确定性规则,但不得发布假产品或宣称真实匹配已完成。短信、外部查询、私有授权与生产队列守护仍为一期未完成项。
|
||||||
|
|||||||
@@ -0,0 +1,281 @@
|
|||||||
|
# 融资需求与评估结果数据契约
|
||||||
|
|
||||||
|
版本:v1.2(需求、完整度/准备状态与报告v2自填支线已实施;匹配仍为计划)。日期:2026-09-24。配套:[开发文档](开发文档.md)、[开发进度](开发进度.md)。
|
||||||
|
|
||||||
|
本文补充融资需求字段、资料完整度与准备等级、产品匹配解释三个范围,作为后续前后端、数据迁移及验收的共同依据。依据为[开发说明书](开发说明书.docx)第2—5节及[原型图](原型.jpg)第02—05、09屏,并遵循用户后续确认的无草稿、H5优先、数据库存元及复用评估/报告表的约定。
|
||||||
|
|
||||||
|
**实施状态:需求v2、用途字典、完整度/准备状态、执行策略快照和报告v2自填支线已实现。** DEC-DEMAND和DEC-READINESS已按用户授权采用首版开发配置;新提交冻结targetResultVersion=2。升级前未带该标记或目标1的输入仍走resultVersion=1,旧成功报告不重算。真实匹配及产品解释待DEC-MATCH/产品资料,v2真实模型语义和生产业务验收仍待完成。本轮使用假模型验证;证据见开发文档0.6和开发进度6.6。
|
||||||
|
|
||||||
|
## 1. 范围与业务含义
|
||||||
|
|
||||||
|
| 概念 | 回答的问题 | 不能表示的含义 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 融资需求 | 用户希望获得多少资金、使用多久、用在哪里 | 银行批准额度、实际利率或放款承诺 |
|
||||||
|
| 资料完整度 | 当前版本列明的自填信息项有多少有效填写 | 真实性、信用评分、所有银行所需材料已齐全 |
|
||||||
|
| 融资准备等级 | 按已发布资料规则,哪些信息缺失、矛盾或可以进入人工核对 | 风险等级、授信等级、获批概率 |
|
||||||
|
| 产品匹配 | 在指定时点、产品版本和已知事实下,具体条件是否满足 | 审批结论;自填条件满足也未经机构核验 |
|
||||||
|
|
||||||
|
本次不新增页面;需求录入放评估页的“资金需求”步骤/分组,确认页、档案、报告同步显示。原型默认展示最多三个候选;不足三个不补造。匹配解释在现有产品详情内展示,传递 assessmentId 与 productId。产品对比、PDF、渠道、注销、知识库等审查发现另外排期,不随本文隐式纳入实现。
|
||||||
|
|
||||||
|
本次只增加共同融资需求字段。年龄段/工作年限、企业开票/纳税/流水/法人等补充资料另在 ASSESS-001 的字段字典阶段细化;当前已有 `year_invoicing/year_tax_amount` 可复用,但没有录入/API支撑时不能启用依赖它们的准入规则或宣称相关完整度已覆盖。企业成立月数与经营月数不等同,不能互相代填。
|
||||||
|
|
||||||
|
## 2. 融资需求输入
|
||||||
|
|
||||||
|
### 2.1 请求与字段
|
||||||
|
|
||||||
|
新增输入格式 `inputVersion:2`,仍使用 `POST /api/v1/assessments`。顶层保留 type、requestId、moneyUnit、form、documents、consentIds;新增 inputVersion:int。下列字段放入 form,个人/企业共用;现有姓名、证件、收入等必填约束本次不放宽。
|
||||||
|
|
||||||
|
| API字段 | 类型/单位 | v2提交约束 | fa_user_info已实现存储 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| requestedAmountYuan | string,元 | 必填,`0.01`—`999999999.99`,规范两位小数,禁止JSON number、指数、负数、逗号、空格 | 新增 requested_amount DECIMAL(11,2) NULL |
|
||||||
|
| requestedTermMonths | int,月 | 必填,1—9999;禁止0、小数、数字字符串;范围为技术表示上限,不表示产品支持如此长期限 | 新增 requested_term_months SMALLINT UNSIGNED NULL |
|
||||||
|
| purposeCode | string,稳定枚举 | 必填,必须在本次类型允许的已发布用途字典内 | 新增 purpose_code VARCHAR(32) NULL,ASCII区分大小写 |
|
||||||
|
| purposeDetail | string或null | purposeCode=other时必填,trim后2—200个字符;禁止HTML/控制字符;其余用途必须null或缺省 | 新增 purpose_detail VARCHAR(200) NULL |
|
||||||
|
| inputVersion(顶层) | int | 新客户端发送2;服务端版本策略见第7节 | 新增 input_version TINYINT UNSIGNED NOT NULL DEFAULT 1 |
|
||||||
|
|
||||||
|
金额继续由客户端字符串运算完成万元→元,服务器以十进制处理;例如 `12.345678 万元` 对应 `123456.78` 元。需求金额不是已有负债、年营收或产品最高额度,不复用这些列。未知需求不以0代填;v2正式提交缺少上述必填字段时返回422,表单仍只保留当前流程内存,不增加保存草稿接口。
|
||||||
|
|
||||||
|
**用途字典已发布:`financing-purposes-20260923-v1`(DEC-DEMAND):**
|
||||||
|
|
||||||
|
| 评估类型 | 允许code及显示名 |
|
||||||
|
| --- | --- |
|
||||||
|
| personal | daily_consumption 日常消费、education 教育进修、home_improvement 家居装修、personal_business 个体经营周转、other 其他用途 |
|
||||||
|
| enterprise | working_capital 经营周转、inventory_purchase 采购备货、equipment_purchase 设备购置、business_expansion 经营扩展、other 其他用途 |
|
||||||
|
|
||||||
|
字典需包含版本、适用type、code、label、requiresDetail和启用状态。`GET /dictionaries` 已返回 assessmentInputVersions、minNewAssessmentInputVersion、financingPurposes及其版本,公开且no-store;当前由服务端配置发布,无后台编辑。移除配置条目即停用新建用途,变更需提升版本。正式提交事务内保存所用字典版本及用途项快照,显示名变化或后续停用不更改已提交记录的用途含义,也不阻止原请求幂等重放;保存位置见第6.2节。用途类别本身不表示机构允许;是否允许由产品规则及正式来源确定,尤其不能把“其他用途”直接判为满足。
|
||||||
|
|
||||||
|
### 2.2 服务端校验与隐私
|
||||||
|
|
||||||
|
- 同一requestId比较规范化的**全部输入**,包括 inputVersion 和四个新字段;缺省可选值归一为null,非法值不得静默丢弃。只允许目标类型的用途code,个人用途不能混入企业提交。
|
||||||
|
- 规范金额格式 `^(?:0|[1-9][0-9]{0,8})\.[0-9]{2}$` 后,再检查严格大于零。客户端展示万元,所有API及JSON快照金额均为元字符串。
|
||||||
|
- 用途说明是用户自由文本,只在本人只读详情及有权限的后台展示;不写请求日志,不作为可执行规则,不直接发送模型。后续报告v2的AI仅可接收经过白名单处理的用途code/标签、需求金额和期限事实;当前v1报告尚未接入这些新事实。说明中即使含“忽略规则”等内容也不影响匹配。
|
||||||
|
- 格式合法但互相矛盾的数据另做一致性校验。例如信用卡已用额度大于总额度时,v2提交返回422 FIELD_INCONSISTENT;不自动改小金额或改成0。不能从“贷款笔数为0”简单推断其他全部债务余额必为0,不做未经定义的跨概念推导。
|
||||||
|
- `documents/consentIds` 当前只能为空。后续接入授权后,拒绝可选征信查询不阻止自填提交,不为未付费查询扣资料完整度分,也不将未查询当成征信正常。
|
||||||
|
|
||||||
|
### 2.3 已实现请求示例
|
||||||
|
|
||||||
|
以下为本地已支持的合成企业请求样例,不代表生产已发布:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"inputVersion": 2,
|
||||||
|
"type": "enterprise",
|
||||||
|
"requestId": "needs_submit_example_001",
|
||||||
|
"moneyUnit": "yuan",
|
||||||
|
"form": {
|
||||||
|
"name": "示例联系人",
|
||||||
|
"phone": "19900000000",
|
||||||
|
"city": "深圳市",
|
||||||
|
"cityCode": "440300",
|
||||||
|
"enterpriseName": "示例企业",
|
||||||
|
"businessCode": "91440300123456789A",
|
||||||
|
"occupation": "科技服务",
|
||||||
|
"operatingMonths": 24,
|
||||||
|
"annualRevenue": "1000000.01",
|
||||||
|
"totalDebtBalance": "0.00",
|
||||||
|
"monthlyLoanPayment": "0.00",
|
||||||
|
"requestedAmountYuan": "123456.78",
|
||||||
|
"requestedTermMonths": 12,
|
||||||
|
"purposeCode": "working_capital",
|
||||||
|
"purposeDetail": null
|
||||||
|
},
|
||||||
|
"documents": [],
|
||||||
|
"consentIds": []
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
HTTP201接受回执保持 `{assessmentId:string,submissionStatus:"accepted"}`;新需求不进入会变化的回执。状态与报告仍通过assessmentId读取。GET详情返回 inputVersion 与四个需求字段,金额不转number;历史v1新增字段null并显示“当时未采集”。
|
||||||
|
|
||||||
|
## 3. 资料完整度
|
||||||
|
|
||||||
|
### 3.1 字段集合与计算
|
||||||
|
|
||||||
|
完整度由服务端确定性计算,保存 policyVersion、适用字段集合、逐项状态和计算时点。前端只渲染服务端结果,不沿用本机表单百分比充当正式报告结果。**首版已采用等权字段计数,不引入风险权重;DEC-READINESS开发版为self-reported-base-20260924-v1,后续变更需版本化发布。**
|
||||||
|
|
||||||
|
已实现首版字段集 `self-reported-base-20260924-v1` 只覆盖已实现资料和新增需求,不声称覆盖说明书尚未接入的全部经营资料:
|
||||||
|
|
||||||
|
| 分组 | 个人适用字段 | 企业适用字段 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 基础描述(2项) | city、occupation | city、occupation(行业) |
|
||||||
|
| 融资需求(3项) | requestedAmountYuan、requestedTermMonths、purposeCode | 同左 |
|
||||||
|
| 收入/经营 | monthlyIncome、socialSecurity、housingFund(3项) | annualRevenue、operatingMonths(2项) |
|
||||||
|
| 债务情况 | totalDebtBalance、monthlyLoanPayment、creditLoanCount、creditCardCount、creditCardTotalLimit、creditCardUsedLimit(6项) | totalDebtBalance、monthlyLoanPayment(2项) |
|
||||||
|
| 条件项 | purposeCode=other时加入purposeDetail(1项) | 同左 |
|
||||||
|
|
||||||
|
因此个人基础分母14、企业基础分母9;选择other各加1。表单无需为达到100%而强迫填写不适用字段。姓名、证件、手机号、企业名称/代码属于身份/提交校验,不计入这个资料完成比例;附件数量、授权选择、第三方付费查询、房车有无均不计分。city有效文本计已填;cityCode未知单独影响地区规则的unknown,不反向把已有城市输入当空。
|
||||||
|
|
||||||
|
逐项 `state=provided|missing|invalid|not_applicable`:
|
||||||
|
|
||||||
|
- provided:适用且通过类型、枚举、范围校验;合法 `0`、`"0.00"`、`none`、`no` 都是有效回答,不因金额小或选择“无”降低完整度。
|
||||||
|
- missing:适用但null/未采集;空字符串在请求校验/规范化时处理,禁止写成“未知”字符串冒充已填。
|
||||||
|
- invalid:历史或内部数据未通过字段校验,不计已填;v2新提交原则上在写库前拒绝。
|
||||||
|
- not_applicable:由已发布的type/条件规则判定,排除分母;不能由客户端传入,也不能为了提高比例自行排除。
|
||||||
|
|
||||||
|
`applicableCount` 为非not_applicable项数,`providedCount`为其中provided项数。`percent=ROUND_HALF_UP(100×providedCount/applicableCount)`,整数0—100,整数/十进制运算,不用二进制浮点。已发布策略的分母为0视为配置错误,任务failed、errorCode=ASSESSMENT_POLICY_INVALID,不生成正式completeness或成功报告,不能给0或100分。未填项按字段清单输出 missingFields;invalidFields单独列出。一致性问题使用 consistencyIssues 记录,不因单字段都已填而消失。
|
||||||
|
|
||||||
|
验证样例:个人普通用途14项中11项provided,返回79;企业9项中8项provided,返回89;全部provided且含债务金额为0时仍100。该比例仅表示当前字段集填写情况,与收入高低、产品数量、查询授权无关。
|
||||||
|
|
||||||
|
### 3.2 完整度DTO
|
||||||
|
|
||||||
|
`completeness={status,policyVersion,providedCount,applicableCount,percent,items,missingFields,invalidFields}`。
|
||||||
|
|
||||||
|
- status:evaluated / policy_unconfirmed / legacy_unavailable。
|
||||||
|
- evaluated时 policyVersion非空,计数/percent为整数,items完整包含 `{fieldKey,label,state}`;字段顺序由策略固定。missingFields/invalidFields为fieldKey数组,可由items复算。
|
||||||
|
- 规则未发布时 status=policy_unconfirmed,policyVersion/计数/percent=null,items与缺失数组为空;前端展示“资料完整度规则待配置”,不展示0分。可以在其他区域继续呈现实际输入和缺失事实。
|
||||||
|
- 历史v1报告不事后添加试算分:适配为legacy_unavailable/null,或沿用v1展示“未计算”。新字段集发布只影响新评估,不重算旧比例。
|
||||||
|
|
||||||
|
## 4. 融资准备等级
|
||||||
|
|
||||||
|
### 4.1 建议策略与发布闸门
|
||||||
|
|
||||||
|
首版采用可解释的资料状态;以下规则已按本轮授权落地为 **DEC-READINESS开发版**,生产业务验收仍需人工样例,不使用A/B/C信用分:
|
||||||
|
|
||||||
|
| 判断顺序 | 条件 | 已实现code / label |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 1 | 尚无已发布的完整度/准备策略,或记录不支持该输入版本 | code=null;状态policy_unconfirmed或legacy_unavailable;显示未评定 |
|
||||||
|
| 2 | 存在invalid字段或已定义的consistencyIssues | needs_verification / 需核实资料 |
|
||||||
|
| 3 | 适用字段存在missing | needs_information / 待完善资料 |
|
||||||
|
| 4 | 当前字段集全部provided,且无上述问题 | ready_for_review / 可进入资料核对 |
|
||||||
|
|
||||||
|
等级依赖实际计数而非四舍五入后的percent,避免99.x%显示100后误升等级。没有任意80/90分门槛;若业务希望使用分档或权重,应先提交版本化规则和人工样例,经确认后另发策略。上述“可进入资料核对”只针对当前自填字段集,即使完整度100也可能缺少银行要求的材料,不代表满足某产品,更不代表已经核验、可以获批或可以申请。
|
||||||
|
|
||||||
|
准备等级不读取候选产品数量、不依赖AI生成成功与否、不以收入额度高低给人分级。外部资料未查询单独写 verificationStatus=not_verified 和风险提示;可选授权的拒绝本身不降级。
|
||||||
|
|
||||||
|
### 4.2 准备等级DTO与解释
|
||||||
|
|
||||||
|
`readiness={status,policyVersion,code,label,basis,verificationStatus}`;status同完整度,未计算时code/label/policyVersion=null、basis=[]。evaluated时保存稳定code、显示标签和 `basis:[{basisId,code,fieldRefs,issueRefs,message}]`,basisId在本报告内唯一且稳定。basis由服务端模板生成,fieldRefs引用本次字段清单,issueRefs引用一致性问题ID。模型可解释既有basis,但无权改code/label/优先级。
|
||||||
|
|
||||||
|
`consistencyIssues:[{id,code,fieldRefs,message}]`只使用已发布校验规则。例如 `CREDIT_CARD_USED_EXCEEDS_LIMIT` 引用creditCardUsedLimit和creditCardTotalLimit。历史异常仍保留原值供本人/有权限后台核对,不用AI猜测正确数值。
|
||||||
|
|
||||||
|
## 5. 产品匹配与结果解释
|
||||||
|
|
||||||
|
### 5.1 候选判定与未知值
|
||||||
|
|
||||||
|
1. 先验证产品发布状态、有效期、适用对象和配置完整性,冻结本次可评估产品集合。草稿/下架/过期产品不进入本次集合;无真实产品或无已发布规则时 matching.status=unavailable。
|
||||||
|
2. 产品所引用字段必须在服务器规则白名单内,有已定义类型/单位/来源;不支持的字段或操作符阻止发布。不得从 condition、grade、recommendation、deprecated 等任意描述文本直接推断准入。
|
||||||
|
3. 每条规则返回 `pass|fail|unknown|not_applicable`。客户端不能指定not_applicable;未知金额、未提供cityCode、未查询的信用信息不能当作0或正常。规则所需证据必须达到 rule.requiredSource=self_reported或verified;自填满足阈值不能冒充verified。
|
||||||
|
4. 硬规则中任一fail → eligibility=ineligible;无fail但任一unknown → unknown;适用硬规则全部pass → eligible。fail与unknown并存时仍ineligible,同时保留未知项。不能把零条有效硬规则当作默认通过:产品必须有有效发布校验及本节需求规则。
|
||||||
|
5. 产品配置缺失/非法与用户信息缺失不同。产品amount/term为null不能当“无限制”,regions=[]是全国、regions=null是未配置。配置损坏时整次匹配failed并记录安全错误,不悄悄跳过产品后宣称完整匹配。用户字段缺失才使用unknown。
|
||||||
|
|
||||||
|
**需求匹配口径:单产品完整覆盖。** `amountMinYuan ≤ requestedAmountYuan ≤ amountMaxYuan`、`termMinMonths ≤ requestedTermMonths ≤ termMaxMonths`,边界包含;禁止自动修改用户需求、拆金额或拼多个产品凑额度。产品用途白名单放入带版本的eligibility_rules;没有明确用途依据则阻止发布,“其他用途”需人工核实,返回unknown,不从purposeDetail自由文本自动推断。
|
||||||
|
|
||||||
|
unknown不是建议申请;ineligible不是永久拒绝。展示应为“本次资料不满足已知条件”或“缺少判断依据”,并说明所用资料未经核验。only eligible进入默认候选列表。
|
||||||
|
|
||||||
|
### 5.2 排序、理由与证据
|
||||||
|
|
||||||
|
- `score` 为number|null。业务评分尚未定稿时始终null,不显示匹配百分比。DEC-MATCH定稿后才允许0—100的规则匹配指标,存scorePolicyVersion;仍不代表审批概率。overallScore也不由模型生成。
|
||||||
|
- 发布时必须提供稳定排序策略版本。建议无评分首版按运营已确认优先级、productId升序排序;如优先级尚未配置,显式使用productId升序作为稳定展示顺序,标注“不代表优劣排序”。不可临时用AI结果排序。
|
||||||
|
- eligible项rank从1递增;ineligible/unknown的rank=null,不进入候选排名。默认最多展示三家且有查看更多;其他结果独立显示“条件不符”与“待补资料”,不混入推荐卡片。若提供额度/利率排序,仅改变客户端显示顺序,不覆盖历史rank。
|
||||||
|
- 推荐依据 `positiveReasons`、不符条件 `blockingReasons`、待核实/注意事项 `cautions` 分开保存。首页各组最多展示三条;没有证据就不生成,详情可读完整列表。通过部分条件的不适配产品,其positiveReasons不能标成“推荐该产品”。
|
||||||
|
- 理由 `{reasonId,code,text,evidenceRefs}` 由确定性规则/模板生成,evidenceRefs至少一个且只引用本项证据。失败条件、未知依据全部保存,不能因为只展示三条而丢掉阻断项。产品公共优势不是用户个性化理由。
|
||||||
|
- 证据 `{evidenceId,ruleId,ruleVersion,kind,fieldRefs,operator,expected,actual,outcome,source,observedAt}` 固定类型、单位和来源。evidenceId在整份报告内唯一且稳定,例如由productId、ruleId和条目序号组成,不能让不同产品的同名规则共用ID。kind=hard/soft,source=self_reported/verified/product_snapshot;expected/actual金额保持元字符串,未知actual=null。规则快照保存requiredSource,复核时能判明证据是否够用。
|
||||||
|
- 单项 `aiExplanation={status,text,evidenceRefs}`,status=not_requested/completed/failed;只解释服务端已生成的理由,不能新增规则、伪造优势/风险或改判定。模型内容缺证据、含审批承诺或改写数值时拒绝保存该段,保留规则理由并显示解释暂不可用。不存在“必须每家补足三正三反”的生成要求。
|
||||||
|
|
||||||
|
### 5.3 结果存储与接口
|
||||||
|
|
||||||
|
仍在 `fa_ai_chat.match_result` 保存有序JSON数组,数据库键沿用snake_case,DTO层转camelCase;不新建匹配表,不把有序数组改为对象。每项包含:
|
||||||
|
|
||||||
|
| API键 | 持久化键 | 类型与约束 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| productId / productVersion | product_id / product_version | string / int;ID传输用字符串,版本为冻结版本 |
|
||||||
|
| eligibility / rank / score | eligibility / rank / score | eligible或ineligible或unknown;rank:int或null;score:number或null |
|
||||||
|
| scorePolicyVersion | score_policy_version | 未评分时null |
|
||||||
|
| reasons | reasons | string[],保留已有键供服务端迁移兼容;由positive/blocking/cautions确定性汇总,不能作为唯一证据 |
|
||||||
|
| positiveReasons / blockingReasons / cautions | positive_reasons / blocking_reasons / cautions | 理由对象数组 |
|
||||||
|
| evidence / aiExplanation | evidence / ai_explanation | 证据数组及可空解释对象,嵌套对象也显式映射键名 |
|
||||||
|
| productSnapshot | product_snapshot | 不可变产品参数、适用对象/地区/用途规则、材料、来源引用、采集/更新时间、产品版本;金额元字符串,期限月,年化百分数字符串 |
|
||||||
|
|
||||||
|
当前 `Assessment::complete()` 用isset强制rank/score非null,**必须随v2调整为“键存在+按eligibility验证类型”**,不能仅在文档中把score写可空。匹配读接口尚未实现,不存在把这一变更当现有兼容已通过的依据。
|
||||||
|
|
||||||
|
聚合对象 `matching={status,reasonCode,ruleVersion,orderingPolicyVersion,catalogSnapshotId,candidateCount,ineligibleCount,unknownCount}` 存在report_data,不放入match_result数组头部:
|
||||||
|
|
||||||
|
- status=unavailable:未运行,reasonCode=PRODUCT_CATALOG_UNAVAILABLE / MATCH_RULES_UNPUBLISHED / LEGACY_INPUT / DEMAND_INCOMPLETE;三个计数为null,match_result=[]。
|
||||||
|
- status=completed:已完成规则执行,计数为非负整数,允许candidateCount=0;应结合另外两个计数解释原因,不能把unknown说成不符合。
|
||||||
|
- status=failed:执行器/存储/配置异常,计数null;整次评估维持failed,不向报告接口暴露部分成功的正式报告。失败信息由状态接口提供,需用户重试。
|
||||||
|
|
||||||
|
未具备真实产品时,可以先成功生成需求/完整度自填报告并标unavailable;这只是内部开发支线,**不改变一期必须完成真实产品匹配及外部渠道验收的既定范围**。
|
||||||
|
|
||||||
|
| 方法/路径(/api/v1前缀) | 实施状态 | data与行为 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| GET /assessments/:id/report | 已支持v1/v2自填支线 | 本人成功记录;resultVersion辨识格式,未完成409、越权/不存在404;no-store |
|
||||||
|
| GET /assessments/:id/matches | 计划新增 | 参数eligibility默认eligible,可选ineligible/unknown;limit默认3、1—50,cursor可空。data={assessmentId,resultVersion,matching,items,hasMore,nextCursor};unavailable返回200+空数组及明确状态,不冒充零候选 |
|
||||||
|
| GET /assessments/:id/matches/:productId | 计划新增 | 只返回本次已评估的产品结果/全部理由/快照;未在快照内或越权404。不能拿公共当前产品临时拼出旧评估结论 |
|
||||||
|
| GET /products/:id | 计划新增 | 公开当前产品资料;无用户等级、理由或匹配分;与个人化结果分离 |
|
||||||
|
|
||||||
|
matches只读成功评估,未成功409;参数错误422。服务端先鉴权归属,再解析游标;游标绑定assessmentId、resultVersion、冻结结果摘要、eligibility和偏移量,非法/不匹配游标422。快照不可变,分页不能混用当前产品列表。v1成功报告也可返回matching.status=unavailable/reasonCode=LEGACY_INPUT;不回填历史match_result。
|
||||||
|
|
||||||
|
当前产品是否下架单列 `currentAvailability=available|unavailable|unknown`,由读取时受限查询得到,不写回productSnapshot、不改历史rank;当前不可用或查询失败时,申请准备页说明状态,不能继续显示“立即申请”。本项目仍只做资料引导,不向银行提交申请。
|
||||||
|
|
||||||
|
## 6. 报告v2与任务快照
|
||||||
|
|
||||||
|
### 6.1 报告组合契约
|
||||||
|
|
||||||
|
`resultVersion=2`、`reportType=financing_needs_analysis`。服务端组装报告:
|
||||||
|
|
||||||
|
| 字段 | 生成方与约束 |
|
||||||
|
| --- | --- |
|
||||||
|
| assessmentId、inputVersion、generatedAt、modelCode | 服务端;modelCode为实际指定模型快照 |
|
||||||
|
| overallScore | 首版null,无已确认评分规则不生成 |
|
||||||
|
| demand | 服务端冻结 requestedAmountYuan/requestedTermMonths/purposeCode/purposeLabel;不含自由说明正文 |
|
||||||
|
| facts | 服务端白名单事实,fieldKey、valueType、value、unit、source、observedAt;金额value为元字符串、月/数量为int、枚举为code、未知null;不含身份资料 |
|
||||||
|
| completeness、readiness、consistencyIssues、matching | 服务端按第3—5节计算;模型无修改权限 |
|
||||||
|
| summary、strengths、improvements、riskWarnings | AI定性文本;summary为string,后三者为 `{text,evidenceRefs}[]`,证据不足可为空;风险提示另含服务端固定“自填未核验”等提示 |
|
||||||
|
| disclaimer、policyVersions、systemWarnings | 服务端固定模板及版本集合;systemWarnings为固定风险提示字符串数组,独立于模型风险引用;政策、字段字典和模型版本不能省略 |
|
||||||
|
|
||||||
|
报告标准区块为“需求概况—资料完整度—准备等级—主要优势—待完善项—风险提示—候选及解释—免责声明”。不必为每个区块生成长文本;缺数据时明确未提供。需求金额/期限、比例、等级、匹配理由通过结构化事实呈现,AI正文继续只作定性解释。
|
||||||
|
|
||||||
|
v2模型输出不直接作为完整Report入库。服务端仅接受固定四个模型字段summary/strengths/improvements/riskWarnings;summary 2—600字符,各数组0—6项,每text 2—400字符,每条evidenceRefs 1—5个且去重,只引用输入的facts/ruleEvidence/readinessBasis公共引用ID。统一引用命名空间 `fact:<fieldKey>`、`rule:<evidenceId>`、`readiness:<basisId>`,使用第4.2/5.2节保存的唯一ID;无引用的优势不能存。保留现有禁止数值复述、HTML和控制字符的校验;新增结构不得沿用当前只支持observations/suggestions/dataGaps的v1解析器。summary仅概括事实且不能改变结构化结论。
|
||||||
|
|
||||||
|
禁止输出保证通过、审批概率、已核验信用良好、内部渠道等无依据结论。语义校验和提示词不能保证模型绝无错误,需用人工金标准样例持续验收;数值、等级和准入结果始终由服务端权威字段控制。主报告结构不合法时任务failed;单产品AI解释失败时保留确定性匹配理由,不触发无限收费重试。
|
||||||
|
|
||||||
|
### 6.2 冻结与持久化
|
||||||
|
|
||||||
|
- 已为 `fa_ai_chat` 新增 `analysis_context JSON NULL`,仅用于正式assessment会话的依据快照,不新建业务表。快照分成不可覆盖的input_snapshot和execution_snapshot两个部分,分别只写入一次。
|
||||||
|
- **提交时冻结输入依据:** 正式提交事务在保存fa_user_info和会话的同时写入input_snapshot,已实现inputVersion、normalizationVersion、targetResultVersion及完整purposeDictionary(version、items,含type/code/label/requiresDetail/enabled)。基础字段规范化由固定版本代码处理,未冻结全部字段校验规则全文;未来变更须保留原版本规范化路径。输入原值仍冻结在fa_user_info,不重复存整包身份资料。此时execution_snapshot=null;用途改名/停用不影响已接受输入,读取和分析使用提交时的标签。快照写入失败则整个提交回滚,不能只保存输入而丢失其解释依据。
|
||||||
|
- **执行时冻结计算依据(新目标v2已实现自填支线):** worker首次抢占后、任何外部调用前,在事务内一次性补入execution_snapshot:目标resultVersion、完整度与准备策略、evaluatedAt和匹配不可用原因。本轮真实产品未就绪,productSnapshots=[]、matchingRules/orderingPolicy=null;真实集合、产品快照、匹配规则全文及排序策略随PRODUCT实施,不伪造快照。沿用input_snapshot的输入含义,不重新套用当前用途启用门槛;没有可用策略也保存明确的unavailable原因。
|
||||||
|
- analysis_context只由服务端写,具有schemaVersion、inputHash、executionHash、contentHash和上述版本集合;以规范化JSON计算SHA-256,输入和执行部分分别校验,executionHash/contentHash在执行部分冻结前为null,冻结后生成且不再变化。快照包含可重现的实际策略内容,不能只留版本号而覆盖后台配置。当前引擎equal-fields-v1的类型范围与舍入算法固定在代码,快照保存definitions/fields/conditionalFields/consistencyRules/readinessOrder/labels及版本;算法变化须支持旧引擎,不原地改旧版本语义。catalogSnapshotId由冻结集合内容摘要形成,不要求新增产品集合表。容量和产品数量须在实际产品规模确定后设置有界限制,超限明确失败,禁止悄悄截断候选范围。
|
||||||
|
- 失败重试在相同assessmentId下复用已经冻结的两部分快照;更换taskId仅代表新执行尝试。若尚未成功冻结execution_snapshot,则下一次在首次可用的抢占阶段补入并记录当时策略版本,不声称采用提交时未取得的规则;input_snapshot始终不变。重复消费不得覆盖任一已有部分,哈希不符时明确失败。旧v1记录的context可继续为null,不伪造历史字典。
|
||||||
|
- 需要更新需求、采用最新产品/规则或补充资料时,创建新assessmentId。成功报告不提供“原地刷新结论”。运行中产品下架不能改写快照;读取时另附currentAvailability。外部数据若后续接入,同样先冻结授权结果的来源/时间,不回写原表单。
|
||||||
|
- 完成事务原子写report_data、match_result、result_version、rule_version、模型快照及succeeded。rule_version标识整套报告策略发布版本;各子策略版本保存在policyVersions/context中。消息与tokens继续写fa_ai_chat_log,未知用量null。
|
||||||
|
- analysis_context不存密钥、证件或重复整包自由文本;可凭assessmentId读取冻结输入。存取权限按评估归属,公共产品API不返回这些context。后台不得修改已提交输入、context或历史结论。
|
||||||
|
|
||||||
|
## 7. 版本兼容与迁移
|
||||||
|
|
||||||
|
1. **目标版本冻结(已实施):** 新提交input_snapshot.targetResultVersion=2;发布前无该标记或显式1则仍使用v1,不因重试升级。`assessment.new_result_version`仅影响新提交,改配置不重算旧报告。
|
||||||
|
|
||||||
|
2. **版本相互独立:** inputVersion是提交字段结构,resultVersion是结果JSON结构,policyVersion是计算策略,productVersion是产品修订;不能只用某一个version替代全部。
|
||||||
|
3. **旧记录不补造:** 新增输入列先可空,input_version默认1,历史需求字段均null,不用收入/负债猜需求;旧报告仍resultVersion=1。v2只在新提交校验中强制需求字段,不能直接给全部历史行加NOT NULL。
|
||||||
|
4. **兼容窗口:** 服务端先支持v1/v2及双版本报告读取,再发布发送v2的前端。部署配置minNewAssessmentInputVersion明确新建最低版本;兼容窗口可为1,切到2后新requestId缺省或显式1返回422 INPUT_VERSION_UNSUPPORTED。切换时点由发布记录确认,不允许永久依赖缺省v1绕过必填。
|
||||||
|
5. **幂等优先:** 鉴权后先定位(userId,requestId)。已有记录按其原inputVersion及冻结的normalizationVersion/输入字典做结构校验、规范化并与原规范值比较;v1使用保留的v1规范化路径。同内容仍HTTP201固定回执,即使当前最低版本为2或原用途已停用;不得重新套用当前字典的启用门槛。输入版本或合法内容冲突409 REQUEST_CONFLICT,格式本身非法仍422。不得先套v2必填而拒绝原v1成功请求的重放。软删除旧键仍409,不能重建。
|
||||||
|
6. **旧重试:** 已有v1失败记录保持v1输入和v1自填分析,不因升级自动获得v2完整度/等级/匹配;迁移时为旧任务选择v1处理路径。若需v2需求分析,用户重新填写并正式提交新记录。旧重试键仍返回原taskId,不换版本重跑。旧报告结构不支持时显示明确格式提示,不用当前规则重新计算。
|
||||||
|
7. **增量迁移(本地已执行,部署其他环境仍须预检):** 先预检版本/异常金额/业务行数,备份目标结构与数据;追加fa_user_info五个字段、fa_ai_chat.analysis_context;新增金额/期限非负和范围CHECK允许历史NULL,用途字典在服务端校验。稳定查询确有需求再加索引,不为每个需求字段预先建索引。迁移工具需可重复执行、校验列类型与约束,不只检查列名。
|
||||||
|
8. **回滚:** 发布前停止或排空旧worker,再部署支持新context和v1/v2的消费者;重启后验证积压任务。回滚优先关闭新建v2、保留新列和兼容读取;存在v2任务/报告时不得直接退回只认v1的旧代码、删列或覆盖备份。恢复备份须先核对备份后新增业务数据,不自动重跑旧空库迁移。
|
||||||
|
|
||||||
|
|
||||||
|
## 8. 验收样例与任务归属
|
||||||
|
|
||||||
|
以下为验收条件全集。需求输入、字典/历史冻结、版本回放及重试保持已由76项专项检查和H5验证;41项基础、29项任务、15项HTTP回归通过。新增54项覆盖完整度/准备状态、执行快照、报告v2与模型引用校验,H5已验证个人79%、企业100%和未发布无分。三态匹配、真实产品证据、v2真实模型语义与渠道验收仍待完成,不能以已通过子集替代。
|
||||||
|
|
||||||
|
| 场景 | 验收结果 | 任务 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 需求金额 | 12.345678万元精确往返123456.78元;0/负数/超上限/JSON number拒绝 | ASSESS-001、ASSESS-003、ASSESS-004 |
|
||||||
|
| 期限与用途 | 期限整数边界通过;0/小数拒绝;other缺说明拒绝,非other带说明拒绝,跨类型用途拒绝 | ASSESS-001、ASSESS-003 |
|
||||||
|
| 原键重放 | 同键同内容仍原回执;只改金额/期限/用途/说明/版本任一项409 | ASSESS-003 |
|
||||||
|
| 用途版本变化 | 提交后改名/停用不改旧标签,不阻断原键重放;新提交按当前发布字典校验 | ASSESS-003、REPORT-002 |
|
||||||
|
| 版本切换 | 最低新建版本2时,旧v1原键可重放,新v1键422;旧报告/失败重试保持v1 | ASSESS-002、ASSESS-003、REPORT-002 |
|
||||||
|
| 完整度 | 个人11/14=79,企业8/9=89;0/none计已填、null不计;other增加条件项 | REPORT-003 |
|
||||||
|
| 等级 | 未发布规则无分数/等级;所有字段已填但一致性异常仍需核实;100%不表示授信 | REPORT-003、REPORT-004 |
|
||||||
|
| 授权与来源 | 拒绝可选查询不扣完整度,不宣称信用正常;verified要求不可用self_reported替代 | FILE-004、PRODUCT-002、REPORT-003 |
|
||||||
|
| 产品边界 | 金额/期限等于上下界通过,超界fail;不拆额度、不改需求;other用途unknown | PRODUCT-002 |
|
||||||
|
| 三态合并 | fail+unknown→ineligible并保留两类证据;无fail有unknown不得进入候选 | PRODUCT-002 |
|
||||||
|
| 配置异常 | 无支持字段/非法操作符/用途或地区配置缺失阻止发布;运行配置损坏或完整度分母0明确failed,不生成成功报告 | PRODUCT-001、PRODUCT-003、REPORT-003 |
|
||||||
|
| 解释证据 | 所有理由可追溯到冻结规则/事实;不凑三条;模型不能改判定或宣称获批 | PRODUCT-004、REPORT-003 |
|
||||||
|
| 空结果 | 未运行unavailable≠已运行零候选≠执行failed;unknown单列显示 | PRODUCT-004、REPORT-004 |
|
||||||
|
| 快照与分页 | 改价/下架/规则更新不改历史;游标绑本人评估和结果;跨账户详情404 | ASSESS-002、PRODUCT-004 |
|
||||||
|
| 历史写入 | score/rank合法null可以写;违规组合拒绝;报告和匹配同事务,无部分成功报告 | REPORT-003 |
|
||||||
|
| 输入最小化 | 模型无手机号/证件/自由用途说明;任意文本不影响规则;错误日志不泄露资料 | ASSESS-003、REPORT-001 |
|
||||||
|
| 任务跨发布 | worker重启、重复消费、过期和重试保持冻结版本,旧taskId无权回写 | REPORT-002 |
|
||||||
|
|
||||||
|
实施顺序:DEC-DEMAND定用途/必填和字段 → ASSESS-001迁移与字典 → ASSESS-003 v2校验和幂等 → ASSESS-002/004确认与历史 → DEC-READINESS与REPORT-003完整度/等级 → DEC-MATCH、PRODUCT-001/002确定性匹配 → PRODUCT-003真实资料发布 → PRODUCT-004/REPORT-004解释和全链路验收。上下游可独立做规则测试,外部数据和真实产品未就绪时保持完整一期未验收。
|
||||||
|
|
||||||
|
本次沿用现有60个任务编号并扩展相关交付项,不把文档完成记成业务完成。原69—117人日是历史全量基线;新增版本兼容、上下文快照和解释交互须在上述决策完成后重估,不声称全部增量已包含原工时,也不把基线直接当最新剩余工作量。
|
||||||
@@ -16,16 +16,23 @@
|
|||||||
import AppIcon from '@/components/AppIcon.uvue'
|
import AppIcon from '@/components/AppIcon.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 { DEFAULT_AVATAR, maskPhone, getAccountProfile, saveAccountProfile, readAvatar } from '@/packages/account/services/account.uts'
|
import { DEFAULT_AVATAR, maskPhone, getAccountProfile, fetchAccountProfile, saveAccountProfile, uploadAccountAvatar } from '@/packages/account/services/account.uts'
|
||||||
const { pageStyle, contentStyle, bottomStyle } = usePageLayout()
|
const { pageStyle, contentStyle, bottomStyle } = usePageLayout()
|
||||||
const nickname = ref(''); const avatar = ref(''); const phone = ref(''); const saving = ref(false); const choosing = ref(false); const errorMessage = ref('')
|
const nickname = ref(''); const avatar = ref(''); const phone = ref(''); const saving = ref(false); const choosing = ref(false); const errorMessage = ref('')
|
||||||
|
const avatarId = ref('')
|
||||||
let originalName = ''; let originalAvatar = ''; let alive = true
|
let originalName = ''; let originalAvatar = ''; let alive = true
|
||||||
|
const dirty = (): boolean => nickname.value.trim() != originalName || avatar.value != originalAvatar
|
||||||
onLoad(() => {
|
onLoad(() => {
|
||||||
if (getSession() == null) { uni.redirectTo({ url: '/packages/account/login/login' }); return }
|
if (getSession() == null) { uni.redirectTo({ url: '/packages/account/login/login' }); return }
|
||||||
const profile = getAccountProfile(); nickname.value = profile.nickname; avatar.value = profile.avatar; phone.value = profile.phone
|
const profile = getAccountProfile(); nickname.value = profile.nickname; avatar.value = profile.avatar; phone.value = profile.phone
|
||||||
originalName = profile.nickname; originalAvatar = profile.avatar
|
originalName = profile.nickname; originalAvatar = profile.avatar
|
||||||
|
// 服务端资料优先;请求返回时已开始编辑则不覆盖用户输入。
|
||||||
|
fetchAccountProfile().then((fresh) => {
|
||||||
|
if (!alive || dirty()) return
|
||||||
|
nickname.value = fresh.nickname; avatar.value = fresh.avatar; phone.value = fresh.phone
|
||||||
|
originalName = fresh.nickname; originalAvatar = fresh.avatar
|
||||||
|
}).catch((error) => { if (alive) errorMessage.value = error instanceof Error ? error.message : '资料加载失败,请重试' })
|
||||||
})
|
})
|
||||||
const dirty = (): boolean => nickname.value.trim() != originalName || avatar.value != originalAvatar
|
|
||||||
const leave = () => { if (getCurrentPages().length > 1) uni.navigateBack(); else uni.redirectTo({ url: '/packages/account/profile/profile' }) }
|
const leave = () => { if (getCurrentPages().length > 1) uni.navigateBack(); else uni.redirectTo({ url: '/packages/account/profile/profile' }) }
|
||||||
const back = () => {
|
const back = () => {
|
||||||
if (saving.value) return
|
if (saving.value) return
|
||||||
@@ -35,14 +42,17 @@
|
|||||||
onBackPress((event): boolean => { if (event.from == 'navigateBack') return false; if (saving.value || dirty()) { back(); return true }; return false })
|
onBackPress((event): boolean => { if (event.from == 'navigateBack') return false; if (saving.value || dirty()) { back(); return true }; return false })
|
||||||
const loadAvatar = async (path: string) => {
|
const loadAvatar = async (path: string) => {
|
||||||
choosing.value = true; errorMessage.value = ''
|
choosing.value = true; errorMessage.value = ''
|
||||||
try { const image = await readAvatar(path); if (alive) avatar.value = image }
|
try {
|
||||||
|
const result = await uploadAccountAvatar(path)
|
||||||
|
if (alive) { avatar.value = result['avatarUrl'] as string; avatarId.value = result['attachmentId'] as string }
|
||||||
|
}
|
||||||
catch (error) { if (alive) errorMessage.value = error instanceof Error ? error.message : '图片读取失败' }
|
catch (error) { if (alive) errorMessage.value = error instanceof Error ? error.message : '图片读取失败' }
|
||||||
finally { if (alive) choosing.value = false }
|
finally { if (alive) choosing.value = false }
|
||||||
}
|
}
|
||||||
const chooseAvatar = () => {
|
const chooseAvatar = () => {
|
||||||
if (saving.value || choosing.value) return
|
if (saving.value || choosing.value) return
|
||||||
uni.showActionSheet({ itemList: ['从相册选择', '恢复默认头像'], success: (action) => {
|
uni.showActionSheet({ itemList: ['从相册选择', '恢复默认头像'], success: (action) => {
|
||||||
if (action.tapIndex == 1) { avatar.value = ''; return }
|
if (action.tapIndex == 1) { avatar.value = ''; avatarId.value = 'default'; return }
|
||||||
uni.chooseImage({ count: 1, sizeType: ['compressed'], sourceType: ['album'], success: (result) => {
|
uni.chooseImage({ count: 1, sizeType: ['compressed'], sourceType: ['album'], success: (result) => {
|
||||||
if (result.tempFilePaths.length == 0) return
|
if (result.tempFilePaths.length == 0) return
|
||||||
if (result.tempFiles[0].size > 512000) { errorMessage.value = '请选择 500KB 以内的图片'; return }
|
if (result.tempFiles[0].size > 512000) { errorMessage.value = '请选择 500KB 以内的图片'; return }
|
||||||
@@ -56,7 +66,7 @@
|
|||||||
errorMessage.value = ''; saving.value = true
|
errorMessage.value = ''; saving.value = true
|
||||||
try {
|
try {
|
||||||
if (getAccountProfile().phone != phone.value) throw new Error('登录状态已变化,请重新进入页面')
|
if (getAccountProfile().phone != phone.value) throw new Error('登录状态已变化,请重新进入页面')
|
||||||
await saveAccountProfile(nickname.value, avatar.value)
|
await saveAccountProfile(nickname.value, avatarId.value)
|
||||||
originalName = nickname.value.trim(); originalAvatar = avatar.value
|
originalName = nickname.value.trim(); originalAvatar = avatar.value
|
||||||
uni.showToast({ title: '资料已保存', icon: 'success' }); leave()
|
uni.showToast({ title: '资料已保存', icon: 'success' }); leave()
|
||||||
} catch (error) { errorMessage.value = error instanceof Error ? error.message : '保存失败,请稍后重试' }
|
} catch (error) { errorMessage.value = error instanceof Error ? error.message : '保存失败,请稍后重试' }
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<scroll-view scroll-y :style="contentStyle(226, false)">
|
<scroll-view scroll-y :style="contentStyle(226, false)">
|
||||||
<view class="profile-content">
|
<view class="profile-content">
|
||||||
<view class="profile-card" @click="openAccount"><view class="avatar"><image class="profile-avatar" :src="avatar.length > 0 ? avatar : DEFAULT_AVATAR" mode="aspectFill" @error="avatar = ''" /></view><view class="profile-main"><text class="profile-name">{{ displayName }}</text><text class="profile-phone">{{ loggedIn ? maskedPhone : '登录后管理您的融资信息' }}</text></view><text v-if="!loggedIn" class="account-state">未登录</text><AppIcon v-if="loggedIn" name="chevron-right" :size="28" /></view>
|
<view class="profile-card" @click="openAccount"><view class="avatar"><image class="profile-avatar" :src="avatar.length > 0 ? avatar : DEFAULT_AVATAR" mode="aspectFill" @error="avatar = ''" /></view><view class="profile-main"><text class="profile-name">{{ displayName }}</text><text class="profile-phone">{{ loggedIn ? maskedPhone : '登录后管理您的融资信息' }}</text></view><text v-if="!loggedIn" class="account-state">未登录</text><AppIcon v-if="loggedIn" name="chevron-right" :size="28" /></view>
|
||||||
<view class="stat-row"><view class="stat-item" @click="go('/packages/finance/archive/archive')"><text class="stat-value">{{ completion }}%</text><text class="stat-label">档案完整度</text></view><view class="stat-item" @click="openReport"><text class="stat-value">{{ hasReport ? '已生成' : '暂无' }}</text><text class="stat-label">评估报告</text></view><view class="stat-item" @click="go('/packages/account/points/points')"><AppIcon name="points" :size="42" /><text class="stat-label">积分中心</text></view></view>
|
<view class="stat-row"><view class="stat-item" @click="go('/packages/finance/archive/archive')"><text class="stat-value">查看</text><text class="stat-label">评估记录</text></view><view class="stat-item" @click="openReport"><text class="stat-value">查看</text><text class="stat-label">评估报告</text></view><view class="stat-item" @click="go('/packages/account/points/points')"><AppIcon name="points" :size="42" /><text class="stat-label">积分中心</text></view></view>
|
||||||
<view class="menu-list"><view class="menu-row" v-for="item in menus" :key="item.name" @click="open(item.key)"><AppIcon :name="item.icon" :size="46" /><text class="menu-name">{{ item.name }}</text><text v-if="item.note.length > 0" class="menu-note">{{ item.note }}</text><AppIcon name="chevron-right" :size="28" /></view></view>
|
<view class="menu-list"><view class="menu-row" v-for="item in menus" :key="item.name" @click="open(item.key)"><AppIcon :name="item.icon" :size="46" /><text class="menu-name">{{ item.name }}</text><text v-if="item.note.length > 0" class="menu-note">{{ item.note }}</text><AppIcon name="chevron-right" :size="28" /></view></view>
|
||||||
<button class="logout" @click="logout">{{ loggedIn ? '退出登录' : '登录 / 注册' }}</button>
|
<button class="logout" @click="logout">{{ loggedIn ? '退出登录' : '登录 / 注册' }}</button>
|
||||||
<text class="version-note">AI融资 · 让融资更简单</text>
|
<text class="version-note">AI融资 · 让融资更简单</text>
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="uts">
|
<script setup lang="uts">
|
||||||
import { getUserStorage, setUserStorage } from '@/services/storage.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'
|
||||||
@@ -23,8 +23,8 @@
|
|||||||
import AppIcon from '@/components/AppIcon.uvue'
|
import AppIcon from '@/components/AppIcon.uvue'
|
||||||
import { getSession } from '@/services/api.uts'
|
import { getSession } from '@/services/api.uts'
|
||||||
import { logoutAccount } from '@/packages/account/services/auth.uts'
|
import { logoutAccount } from '@/packages/account/services/auth.uts'
|
||||||
import { activeAssessmentType, assessmentProgress, showAgreementNotice } from '@/services/assessment.uts'
|
import { showAgreementNotice } from '@/services/assessment.uts'
|
||||||
import { DEFAULT_AVATAR, getAccountProfile, maskPhone } from '@/packages/account/services/account.uts'
|
import { DEFAULT_AVATAR, getAccountProfile, fetchAccountProfile, maskPhone } from '@/packages/account/services/account.uts'
|
||||||
const { contentStyle, pageStyle } = usePageLayout()
|
const { contentStyle, pageStyle } = usePageLayout()
|
||||||
type ProfileMenu = { icon: string, name: string, note: string, key: string }
|
type ProfileMenu = { icon: string, name: string, note: string, key: string }
|
||||||
const menus: ProfileMenu[] = [{ icon: 'archive', name: '我的融资档案', note: '', key: 'archive' }, { icon: 'shield', name: '账户安全', note: '', key: 'security' }, { icon: 'privacy', name: '隐私与授权', note: '', key: 'privacy' }, { icon: 'help', name: '帮助与反馈', note: '', key: 'help' }, { icon: 'info', name: '关于我们', note: 'V1.0.0', key: 'about' }]
|
const menus: ProfileMenu[] = [{ icon: 'archive', name: '我的融资档案', note: '', key: 'archive' }, { icon: 'shield', name: '账户安全', note: '', key: 'security' }, { icon: 'privacy', name: '隐私与授权', note: '', key: 'privacy' }, { icon: 'help', name: '帮助与反馈', note: '', key: 'help' }, { icon: 'info', name: '关于我们', note: 'V1.0.0', key: 'about' }]
|
||||||
@@ -32,32 +32,29 @@
|
|||||||
const displayName = ref('登录 / 注册')
|
const displayName = ref('登录 / 注册')
|
||||||
const maskedPhone = ref('')
|
const maskedPhone = ref('')
|
||||||
const avatar = ref('')
|
const avatar = ref('')
|
||||||
const completion = ref(0)
|
|
||||||
const hasReport = ref(false)
|
|
||||||
onShow(() => {
|
onShow(() => {
|
||||||
const session = getSession()
|
const session = getSession()
|
||||||
loggedIn.value = session != null
|
loggedIn.value = session != null
|
||||||
const profile = getAccountProfile()
|
const profile = getAccountProfile()
|
||||||
maskedPhone.value = maskPhone(profile.phone); avatar.value = loggedIn.value ? profile.avatar : ''
|
maskedPhone.value = maskPhone(profile.phone); avatar.value = loggedIn.value ? profile.avatar : ''
|
||||||
const type = activeAssessmentType()
|
|
||||||
displayName.value = loggedIn.value ? profile.nickname : '登录 / 注册'
|
displayName.value = loggedIn.value ? profile.nickname : '登录 / 注册'
|
||||||
completion.value = assessmentProgress(type).percent
|
if (loggedIn.value) fetchAccountProfile().then((fresh) => {
|
||||||
const latest = getUserStorage('latestAssessment')
|
displayName.value = fresh.nickname; avatar.value = fresh.avatar; maskedPhone.value = maskPhone(fresh.phone)
|
||||||
hasReport.value = latest != null && typeof latest == 'object'
|
}).catch(() => { /* 读取失败保留已归属当前账户的缓存,不显示其他账户资料。 */ })
|
||||||
})
|
})
|
||||||
const go = (url: string) => { uni.navigateTo({ url }) }
|
const go = (url: string) => { uni.navigateTo({ url }) }
|
||||||
const openReport = () => { if (hasReport.value) go('/packages/finance/report/report'); else uni.showModal({ title: '暂无评估报告', content: '完善融资信息后,即可体验融资评估与方案匹配。', confirmText: '去完善', success: (result) => { if (result.confirm) go('/packages/finance/archive/archive') } }) }
|
const openReport = () => { go('/packages/finance/archive/archive') }
|
||||||
const open = (key: string) => {
|
const open = (key: string) => {
|
||||||
if (key == 'archive') go('/packages/finance/archive/archive')
|
if (key == 'archive') go('/packages/finance/archive/archive')
|
||||||
else if (key == 'help') go('/packages/content/help/help')
|
else if (key == 'help') go('/packages/content/help/help')
|
||||||
else if (key == 'security') go(loggedIn.value ? '/packages/account/security/security' : '/packages/account/login/login')
|
else if (key == 'security') go(loggedIn.value ? '/packages/account/security/security' : '/packages/account/login/login')
|
||||||
else if (key == 'privacy') uni.showActionSheet({ itemList: ['查看隐私说明', '管理资料与授权'], success: (result) => { if (result.tapIndex == 0) showAgreementNotice('隐私说明'); else if (result.tapIndex == 1) { setUserStorage('assessmentEditStep', 3); go('/packages/finance/assessment/assessment?type=' + activeAssessmentType()) } } })
|
else if (key == 'privacy') uni.showActionSheet({ itemList: ['查看隐私说明', '管理资料与授权'], success: (result) => { if (result.tapIndex == 0) showAgreementNotice('隐私说明'); else if (result.tapIndex == 1) { go('/packages/finance/archive/archive') } } })
|
||||||
else if (key == 'about') uni.showModal({ title: '关于 AI融资', content: '版本 V1.0.0\n帮助您整理融资信息、了解融资知识与融资方案。评估结果仅供参考,不构成授信承诺。', showCancel: false, confirmText: '我知道了' })
|
else if (key == 'about') uni.showModal({ title: '关于 AI融资', content: '版本 V1.0.0\n帮助您整理融资信息、了解融资知识与融资方案。评估结果仅供参考,不构成授信承诺。', showCancel: false, confirmText: '我知道了' })
|
||||||
}
|
}
|
||||||
const openAccount = () => { go(loggedIn.value ? '/packages/account/edit-profile/edit-profile' : '/packages/account/login/login') }
|
const openAccount = () => { go(loggedIn.value ? '/packages/account/edit-profile/edit-profile' : '/packages/account/login/login') }
|
||||||
const logout = () => {
|
const logout = () => {
|
||||||
if (!loggedIn.value) { go('/packages/account/login/login'); return }
|
if (!loggedIn.value) { go('/packages/account/login/login'); return }
|
||||||
uni.showModal({ title: '退出登录', content: '退出后,本机保存的评估草稿仍会保留。', confirmText: '退出登录', success: (result) => { if (result.confirm) {
|
uni.showModal({ title: '退出登录', content: '将清除本机账户缓存和未提交的填写内容,已提交的评估记录不受影响。', confirmText: '退出登录', success: (result) => { if (result.confirm) {
|
||||||
logoutAccount().then(() => { uni.reLaunch({ url: '/packages/account/login/login' }) })
|
logoutAccount().then(() => { uni.reLaunch({ url: '/packages/account/login/login' }) })
|
||||||
.catch(() => { uni.showToast({ title: '退出失败,请检查网络后重试', icon: 'none' }) })
|
.catch(() => { uni.showToast({ title: '退出失败,请检查网络后重试', icon: 'none' }) })
|
||||||
} }, fail: (error) => { console.error('logout confirmation failed', error); uni.showToast({ title: '暂时无法打开确认框,请重试', icon: 'none' }) } })
|
} }, fail: (error) => { console.error('logout confirmation failed', error); uni.showToast({ title: '暂时无法打开确认框,请重试', icon: 'none' }) } })
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { getUserStorage, setUserStorage } from '@/services/storage.uts'
|
import { getUserStorage, setUserStorage } from '@/services/storage.uts'
|
||||||
import { API_BASE_URL, getSession, requestJson } from '@/services/api.uts'
|
import { getSession, requestJson, uploadJson } from '@/services/api.uts'
|
||||||
import { activeAssessmentType, loadAssessmentDraft, draftText } from '@/services/assessment.uts'
|
import { draftText } from '@/services/assessment.uts'
|
||||||
|
|
||||||
export type AccountProfile = { nickname: string, avatar: string, phone: string }
|
export type AccountProfile = { nickname: string, avatar: string, phone: string }
|
||||||
export const DEFAULT_AVATAR = '/packages/account/static/default-avatar.png'
|
export const DEFAULT_AVATAR = '/packages/account/static/default-avatar.png'
|
||||||
@@ -27,9 +27,6 @@ export function getAccountProfile(): AccountProfile {
|
|||||||
const saved = getUserStorage(profileKey(phone))
|
const saved = getUserStorage(profileKey(phone))
|
||||||
if (saved != null && typeof saved == 'object') {
|
if (saved != null && typeof saved == 'object') {
|
||||||
nickname = draftText(saved as UTSJSONObject, 'nickname'); avatar = draftText(saved as UTSJSONObject, 'avatar')
|
nickname = draftText(saved as UTSJSONObject, 'nickname'); avatar = draftText(saved as UTSJSONObject, 'avatar')
|
||||||
} else {
|
|
||||||
const draft = loadAssessmentDraft(activeAssessmentType())
|
|
||||||
if (draft != null && draftText(draft, 'phone') == phone && draftText(draft, 'name').length > 0) nickname = draftText(draft, 'name')
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { phone, nickname: nickname.length > 0 ? nickname : '融资用户', avatar }
|
return { phone, nickname: nickname.length > 0 ? nickname : '融资用户', avatar }
|
||||||
@@ -40,13 +37,32 @@ async function accountRequest(path: string, method: RequestMethod, payload: UTSJ
|
|||||||
requireAccount()
|
requireAccount()
|
||||||
return await requestJson(path, method, payload)
|
return await requestJson(path, method, payload)
|
||||||
}
|
}
|
||||||
export async function saveAccountProfile(nickname: string, avatar: string): Promise<void> {
|
function cacheProfile(result: UTSJSONObject, phone: string): AccountProfile {
|
||||||
|
const profile: AccountProfile = { phone, nickname: draftText(result, 'nickname'), avatar: draftText(result, 'avatarUrl') }
|
||||||
|
setUserStorage(profileKey(phone), { nickname: profile.nickname, avatar: profile.avatar })
|
||||||
|
return profile
|
||||||
|
}
|
||||||
|
export async function fetchAccountProfile(): Promise<AccountProfile> {
|
||||||
|
const session = requireAccount()
|
||||||
|
const result = await accountRequest('/account/profile', 'GET', {})
|
||||||
|
if (!sameAccount(session)) throw new Error('登录状态已变化,请重新进入页面')
|
||||||
|
return cacheProfile(result, draftText(session, 'phone'))
|
||||||
|
}
|
||||||
|
export async function saveAccountProfile(nickname: string, avatarId: string): Promise<void> {
|
||||||
const session = requireAccount(); const phone = draftText(session, 'phone')
|
const session = requireAccount(); const phone = draftText(session, 'phone')
|
||||||
const name = nickname.trim()
|
const name = nickname.trim()
|
||||||
if (name.length < 2 || name.length > 20) throw new Error('昵称请输入 2–20 个字符')
|
if (name.length < 2 || name.length > 20) throw new Error('昵称请输入 2–20 个字符')
|
||||||
if (API_BASE_URL.length > 0) await accountRequest('/account/profile', 'PUT', { nickname: name, avatar })
|
const payload: UTSJSONObject = { nickname: name }
|
||||||
|
if (avatarId == 'default') payload['avatarId'] = null
|
||||||
|
else if (avatarId.length > 0) payload['avatarId'] = avatarId
|
||||||
|
const result = await accountRequest('/account/profile', 'PUT', payload)
|
||||||
if (!sameAccount(session)) throw new Error('登录状态已变化,请重新进入页面')
|
if (!sameAccount(session)) throw new Error('登录状态已变化,请重新进入页面')
|
||||||
setUserStorage(profileKey(phone), { nickname: name, avatar })
|
cacheProfile(result, phone)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function uploadAccountAvatar(path: string): Promise<UTSJSONObject> {
|
||||||
|
requireAccount()
|
||||||
|
return await uploadJson('/account/avatar', path)
|
||||||
}
|
}
|
||||||
export async function requestSecurityCode(purpose: string, newPhone: string): Promise<void> {
|
export async function requestSecurityCode(purpose: string, newPhone: string): Promise<void> {
|
||||||
if (!['password', 'phone-current', 'phone-new'].includes(purpose)) throw new Error('验证码用途无效')
|
if (!['password', 'phone-current', 'phone-new'].includes(purpose)) throw new Error('验证码用途无效')
|
||||||
@@ -68,27 +84,3 @@ export async function changeAccountPhone(newPhone: string, currentCode: string,
|
|||||||
if (!validCode(currentCode) || !validCode(newCode)) throw new Error('请输入两组 6 位数字验证码')
|
if (!validCode(currentCode) || !validCode(newCode)) throw new Error('请输入两组 6 位数字验证码')
|
||||||
await accountRequest('/account/phone', 'PUT', { newPhone, currentCode, newCode })
|
await accountRequest('/account/phone', 'PUT', { newPhone, currentCode, newCode })
|
||||||
}
|
}
|
||||||
|
|
||||||
// 限制头像体积,避免把临时路径或过大的图片写入账户缓存。
|
|
||||||
export function readAvatar(path: string): Promise<string> {
|
|
||||||
return new Promise<string>((resolve, reject) => {
|
|
||||||
// #ifdef WEB
|
|
||||||
fetch(path).then((response) => response.blob()).then((blob) => {
|
|
||||||
if (blob.size > 512000 || !['image/jpeg', 'image/png', 'image/webp'].includes(blob.type)) { reject(new Error('请选择 500KB 以内的 JPG、PNG 或 WebP 图片')); return }
|
|
||||||
const reader = new FileReader()
|
|
||||||
reader.onload = () => resolve(reader.result as string)
|
|
||||||
reader.onerror = () => reject(new Error('图片读取失败,请重新选择'))
|
|
||||||
reader.readAsDataURL(blob)
|
|
||||||
}).catch(() => reject(new Error('图片读取失败,请重新选择')))
|
|
||||||
// #endif
|
|
||||||
// #ifndef WEB
|
|
||||||
uni.getFileSystemManager().readFile({ filePath: path, encoding: 'base64', success: (result) => {
|
|
||||||
const data = result.data as string
|
|
||||||
if (data.length > 683000) { reject(new Error('请选择 500KB 以内的图片')); return }
|
|
||||||
const mime = data.startsWith('/9j/') ? 'image/jpeg' : (data.startsWith('iVBOR') ? 'image/png' : (data.startsWith('UklGR') ? 'image/webp' : ''))
|
|
||||||
if (mime.length == 0) { reject(new Error('请选择 JPG、PNG 或 WebP 图片')); return }
|
|
||||||
resolve('data:' + mime + ';base64,' + data)
|
|
||||||
}, fail: () => reject(new Error('图片读取失败,请重新选择')) })
|
|
||||||
// #endif
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,37 +1,69 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="analyzing-page" :style="pageStyle()">
|
<view class="analyzing-page" :style="pageStyle()">
|
||||||
<view class="safe" :style="statusStyle(80)"></view>
|
<view class="safe" :style="statusStyle(80)"></view>
|
||||||
<text class="analyzing-title">AI分析中</text>
|
<text class="analyzing-title">{{ title }}</text>
|
||||||
<text class="analyzing-sub">正在整理融资评估信息…</text>
|
<text class="analyzing-sub">正式提交记录 · {{ assessmentId }}</text>
|
||||||
<view class="orbital"><view class="orbit orbit-one"></view><view class="orbit orbit-two"></view><view class="ai-core"><image src="/static/mobile/images/robot-cutout.png" mode="aspectFit" class="ai-core-image"></image></view></view>
|
<view class="orbital"><view class="orbit orbit-one"></view><view class="orbit orbit-two"></view><view class="ai-core"><image src="/static/mobile/images/robot-cutout.png" mode="aspectFit" class="ai-core-image"></image></view></view>
|
||||||
<view class="analysis-list"><view class="analysis-item" v-for="(item,index) in items" :key="item"><view :class="progress > index ? 'analysis-check done' : 'analysis-check'"><text class="analysis-mark">{{ progress > index ? '✓' : '·' }}</text></view><text class="analysis-label">{{ item }}</text></view></view>
|
<text class="status-message">{{ message }}</text>
|
||||||
<text class="estimate">正在生成评估报告,请稍候…</text>
|
<text v-if="error.length > 0" class="status-message">{{ error }}</text>
|
||||||
<text class="view-now" @click="finish">查看评估结果</text>
|
<text v-if="canRetry" class="view-now" @click="retry">{{ retrying ? '正在提交…' : '重新分析' }}</text>
|
||||||
|
<text v-if="succeeded" class="view-now" @click="report">查看分析报告</text>
|
||||||
|
<text class="view-now" @click="loadStatus">刷新状态</text>
|
||||||
|
<text class="view-now" @click="archive">查看已提交资料</text>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="uts">
|
<script setup lang="uts">
|
||||||
import { usePageLayout } from '@/services/layout.uts'
|
import { usePageLayout } from '@/services/layout.uts'
|
||||||
const { statusStyle, pageStyle } = usePageLayout()
|
import { getSession } from '@/services/api.uts'
|
||||||
const progress = ref(0)
|
import { fetchAssessmentStatus, retryAssessment } from '@/packages/finance/services/records.uts'
|
||||||
const items = ['数据解析中', '信用评估中', '匹配方案中', '风险检测中']
|
const { statusStyle, pageStyle } = usePageLayout()
|
||||||
let timer = 0
|
const title = ref('正在读取评估状态')
|
||||||
let finishTimer = 0
|
const message = ref(''); const error = ref(''); const assessmentId = ref('')
|
||||||
let finished = false
|
const canRetry = ref(false); const succeeded = ref(false); const retrying = ref(false)
|
||||||
const finish = () => {
|
let retryKey = ''; let retryTask = ''; let retryOwner = ''
|
||||||
if (finished) return
|
let timer = 0; let visible = false; let generation = 0
|
||||||
finished = true
|
async function loadStatus(): Promise<void> {
|
||||||
clearInterval(timer)
|
clearTimeout(timer)
|
||||||
clearTimeout(finishTimer)
|
if (assessmentId.value.length == 0) { title.value = '缺少评估记录'; message.value = '请从融资档案选择已提交的记录。'; return }
|
||||||
uni.redirectTo({ url: '/packages/finance/report/report' })
|
const session = getSession()
|
||||||
}
|
if (session == null) { uni.redirectTo({ url: '/packages/account/login/login' }); return }
|
||||||
onShow(() => {
|
const owner = session['token'] as string
|
||||||
if (finished) return
|
const request = ++generation
|
||||||
timer = setInterval(() => { if (progress.value < 4) progress.value++; else clearInterval(timer) }, 550)
|
error.value = ''
|
||||||
finishTimer = setTimeout(() => finish(), 3200)
|
try {
|
||||||
})
|
const result = await fetchAssessmentStatus(assessmentId.value)
|
||||||
onHide(() => { clearInterval(timer); clearTimeout(finishTimer) })
|
if (!visible || request != generation || getSession()?.getString('token') != owner) return
|
||||||
onUnmounted(() => { clearInterval(timer); clearTimeout(finishTimer) })
|
canRetry.value = result.canRetry; succeeded.value = result.status == 'succeeded'
|
||||||
|
if (retryTask != result.taskId || retryOwner != owner) { retryKey = ''; retryTask = result.taskId; retryOwner = owner }
|
||||||
|
title.value = result.statusLabel
|
||||||
|
message.value = result.message.length > 0 ? result.message : (result.status == 'processing' ? (result.stage == 'queued' ? '资料已保存,等待分析。您可以稍后从档案查看。' : '正在分析自填资料,您可以稍后从档案查看。') : '分析已完成,可查看本次报告。')
|
||||||
|
// 只轮询服务端仍在处理的任务,不用计时器模拟完成。
|
||||||
|
if (result.status == 'processing') timer = setTimeout(() => { loadStatus() }, 5000)
|
||||||
|
} catch (err) { if (visible && request == generation) { title.value = '状态加载失败'; error.value = err instanceof Error ? err.message : '请稍后重试' } }
|
||||||
|
}
|
||||||
|
const retry = async () => {
|
||||||
|
if (retrying.value || !canRetry.value) return
|
||||||
|
const session = getSession()
|
||||||
|
if (session == null) { loadStatus(); return }
|
||||||
|
const owner = session['token'] as string
|
||||||
|
if (retryOwner != owner) { retryKey = ''; retryOwner = owner }
|
||||||
|
// 网络失败保留当前键;状态中的任务ID变化后才为下次尝试生成新键。
|
||||||
|
if (retryKey.length == 0) retryKey = 'retry_' + Date.now().toString() + '_' + Math.random().toString(36).substring(2, 14)
|
||||||
|
retrying.value = true; error.value = ''; clearTimeout(timer)
|
||||||
|
const request = ++generation
|
||||||
|
try {
|
||||||
|
await retryAssessment(assessmentId.value, retryKey)
|
||||||
|
if (visible && request == generation && getSession()?.getString('token') == owner) { canRetry.value = false; await loadStatus() }
|
||||||
|
} catch (err) { if (visible && request == generation) error.value = err instanceof Error ? err.message : '重新分析失败,请重试' }
|
||||||
|
finally { retrying.value = false }
|
||||||
|
}
|
||||||
|
const report = () => { uni.navigateTo({ url: '/packages/finance/report/report?assessmentId=' + encodeURIComponent(assessmentId.value) }) }
|
||||||
|
const archive = () => { uni.reLaunch({ url: '/packages/finance/archive/archive?assessmentId=' + encodeURIComponent(assessmentId.value) }) }
|
||||||
|
onLoad((options) => { if (options != null) assessmentId.value = options['assessmentId'] ?? '' })
|
||||||
|
onShow(() => { visible = true; loadStatus() })
|
||||||
|
onHide(() => { visible = false; generation++; clearTimeout(timer); canRetry.value = false; succeeded.value = false; message.value = ''; error.value = '' })
|
||||||
|
onUnload(() => { visible = false; generation++; clearTimeout(timer) })
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@@ -51,6 +83,7 @@
|
|||||||
.analysis-mark { font-size: 24rpx; color: #ffffff; }
|
.analysis-mark { font-size: 24rpx; color: #ffffff; }
|
||||||
.analysis-check { width: 38rpx; height: 38rpx; border-radius: 50%; border: 2rpx solid #4c73a7; align-items: center; justify-content: center; margin-right: 17rpx; }
|
.analysis-check { width: 38rpx; height: 38rpx; border-radius: 50%; border: 2rpx solid #4c73a7; align-items: center; justify-content: center; margin-right: 17rpx; }
|
||||||
.analysis-check.done { background-color: #158aff; border-color: #41d7ff; color: white; }
|
.analysis-check.done { background-color: #158aff; border-color: #41d7ff; color: white; }
|
||||||
|
.status-message { margin: 32rpx; color: #d8e6ff; font-size: 28rpx; line-height: 44rpx; text-align: center; }
|
||||||
.estimate { margin-top: 64rpx; color: #8faeda; font-size: 22rpx; }
|
.estimate { margin-top: 64rpx; color: #8faeda; font-size: 22rpx; }
|
||||||
.view-now { margin-top: 28rpx; color: #62c8ff; font-size: 23rpx; }
|
.view-now { margin-top: 28rpx; color: #62c8ff; font-size: 23rpx; }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,65 +1,117 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="page" :style="pageStyle()">
|
<view class="page" :style="pageStyle()">
|
||||||
<AppHeader title="我的融资档案" />
|
<AppHeader :title="selectedId.length > 0 ? '评估记录详情' : '我的融资档案'" :customBack="selectedId.length > 0" @back="backToList" />
|
||||||
<scroll-view scroll-y :style="contentStyle(96, false)"><view class="archive-content">
|
<scroll-view scroll-y :style="contentStyle(0, false)">
|
||||||
<view class="archive-switch"><text :class="type == 'personal' ? 'switch-item switch-active' : 'switch-item'" @click="switchType('personal')">个人档案</text><text :class="type == 'enterprise' ? 'switch-item switch-active' : 'switch-item'" @click="switchType('enterprise')">企业档案</text></view>
|
<view class="archive-content">
|
||||||
<view class="completion"><view class="completion-head"><text class="completion-title">档案完整度</text><text class="completion-score">{{ progress.percent }}<text class="score-unit">%</text></text></view><view class="progress-track"><view class="progress-bar" :style="'width:' + progress.percent + '%;'"></view></view><text class="completion-desc">{{ progress.percent == 100 ? '基础信息已完善,可继续查看评估方案。' : '完善基础信息,让融资评估更贴近您的情况。' }}</text></view>
|
<text class="archive-title">{{ selectedId.length > 0 ? '已提交的评估资料' : '已提交的评估记录' }}</text>
|
||||||
<view class="archive-list"><view class="archive-row" v-for="item in archiveItems" :key="item.step" @click="edit(item.step)"><AppIcon :name="item.icon" :size="48" /><view class="archive-labels"><text class="archive-name">{{ item.name }}</text><text class="archive-note">{{ item.note }}</text></view><text :class="item.done ? 'state state-done' : 'state'">{{ item.done ? (item.step == 3 ? '已选择' : '已完善') : (item.step == 3 ? '选填' : '待完善') }}</text><AppIcon name="chevron-right" :size="28" /></view></view>
|
<text class="archive-note">提交后的资料不可修改;再次评估请新建记录。未提交的填写内容不会保存。</text>
|
||||||
<button class="primary-button complete-button" @click="complete">{{ progress.percent == 100 ? '查看并确认信息' : '继续完善档案' }}</button>
|
<view v-if="selectedId.length == 0" class="start-actions">
|
||||||
<text class="local-note">草稿保存在当前设备,资料与授权可自主选择。</text>
|
<button class="primary-button start-button" @click="start('personal')">个人评估</button>
|
||||||
</view></scroll-view>
|
<button class="primary-button start-button" @click="start('enterprise')">企业评估</button>
|
||||||
</view>
|
</view>
|
||||||
|
<view v-if="error.length > 0" class="notice"><text>{{ error }}</text><text class="action" @click="refresh">重试加载</text></view>
|
||||||
|
<text v-if="loading" class="archive-note">正在加载…</text>
|
||||||
|
<template v-if="selectedId.length > 0 && detail != null">
|
||||||
|
<text class="record-title">{{ detail!.record.type == 'personal' ? '个人' : '企业' }}评估 · #{{ detail!.record.id }}</text>
|
||||||
|
<text class="record-status">{{ detail!.record.statusLabel }}</text>
|
||||||
|
<text class="archive-note">{{ detail!.record.message }}</text>
|
||||||
|
<text class="record-time">提交时间:{{ detail!.record.submittedAt }}</text>
|
||||||
|
<text class="action" @click="openAnalysis">{{ detail!.record.status == 'succeeded' ? '查看分析报告' : '查看分析状态 / 重试' }}</text>
|
||||||
|
<view class="detail-row" v-for="field in detail!.fields" :key="field.label"><text class="field-label">{{ field.label }}</text><text class="field-value">{{ field.value }}</text></view>
|
||||||
|
<text class="action" @click="backToList">返回记录列表</text>
|
||||||
|
</template>
|
||||||
|
<template v-if="selectedId.length == 0">
|
||||||
|
<text v-if="!loading && error.length == 0 && records.length == 0" class="empty-state">暂无已提交的评估记录</text>
|
||||||
|
<view class="record-card" v-for="item in records" :key="item.id" @click="openRecord(item.id)">
|
||||||
|
<text class="record-title">{{ item.type == 'personal' ? '个人' : '企业' }}评估 · #{{ item.id }}</text>
|
||||||
|
<text class="record-status">{{ item.statusLabel }}</text>
|
||||||
|
<text class="record-time">{{ item.city }} · {{ item.submittedAt }}</text>
|
||||||
|
<text class="action">查看已提交资料</text>
|
||||||
|
</view>
|
||||||
|
<text v-if="hasMore && !loading" class="action" @click="loadMore">加载更多</text>
|
||||||
|
<text v-if="records.length > 0 && !hasMore && !loading" class="archive-note">已显示全部记录</text>
|
||||||
|
</template>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="uts">
|
<script setup lang="uts">
|
||||||
import { setUserStorage } from '@/services/storage.uts'
|
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 AppHeader from '@/components/AppHeader.uvue'
|
import { clearAssessmentFlow } from '@/services/assessment-flow.uts'
|
||||||
import AppIcon from '@/components/AppIcon.uvue'
|
import { AssessmentRecord, AssessmentDetail, fetchAssessments, fetchAssessmentDetail } from '@/packages/finance/services/records.uts'
|
||||||
import { AssessmentProgress, activeAssessmentType, assessmentProgress } from '@/services/assessment.uts'
|
const { pageStyle, contentStyle } = usePageLayout()
|
||||||
const { pageStyle, contentStyle } = usePageLayout()
|
const records = ref<AssessmentRecord[]>([])
|
||||||
type ArchiveItem = { icon: string, name: string, note: string, done: boolean, step: number }
|
const detail = ref<AssessmentDetail | null>(null)
|
||||||
const type = ref('personal')
|
const selectedId = ref(''); const loading = ref(false); const error = ref(''); const hasMore = ref(false)
|
||||||
const progress = ref<AssessmentProgress>({ percent: 0, basicDone: false, incomeDone: false, assetsDone: false, authorizationSelected: false })
|
let cursor = ''; let generation = 0; let visible = false
|
||||||
const archiveItems = computed((): ArchiveItem[] => [
|
const sameOwner = (token: string): boolean => { const session = getSession(); return session != null && session['token'] == token }
|
||||||
{ icon: 'file-text', name: '基本信息', note: type.value == 'enterprise' ? '企业与联系人信息' : '身份与联系方式', done: progress.value.basicDone, step: 0 },
|
const refresh = async () => {
|
||||||
{ icon: 'income', name: type.value == 'enterprise' ? '经营信息' : '职业与收入', note: type.value == 'enterprise' ? '经营行业、年限与营收' : '职业、收入与缴纳情况', done: progress.value.incomeDone, step: 1 },
|
const session = getSession()
|
||||||
{ icon: 'asset', name: '资产与负债', note: '贷款、信用卡额度与房车情况', done: progress.value.assetsDone, step: 2 },
|
if (session == null) { records.value = []; detail.value = null; uni.redirectTo({ url: '/packages/account/login/login' }); return }
|
||||||
{ icon: 'authorization', name: '资料与授权', note: '本地资料与数据授权意向', done: progress.value.authorizationSelected, step: 3 }
|
const owner = session['token'] as string
|
||||||
])
|
const request = ++generation
|
||||||
const refresh = () => { progress.value = assessmentProgress(type.value) }
|
loading.value = true; error.value = ''
|
||||||
onShow(() => { type.value = activeAssessmentType(); refresh() })
|
try {
|
||||||
const switchType = (next: string) => { type.value = next; setUserStorage('assessmentActiveType', next); refresh() }
|
if (selectedId.value.length > 0) {
|
||||||
const edit = (step: number) => { setUserStorage('assessmentEditStep', step); uni.navigateTo({ url: '/packages/finance/assessment/assessment?type=' + type.value }) }
|
const result = await fetchAssessmentDetail(selectedId.value)
|
||||||
const complete = () => {
|
if (visible && request == generation && sameOwner(owner)) detail.value = result
|
||||||
if (progress.value.percent == 100) uni.navigateTo({ url: '/packages/finance/confirm/confirm' })
|
} else {
|
||||||
else if (!progress.value.basicDone) edit(0)
|
const result = await fetchAssessments('')
|
||||||
else if (!progress.value.incomeDone) edit(1)
|
if (visible && request == generation && sameOwner(owner)) { records.value = result.items; hasMore.value = result.hasMore; cursor = result.nextCursor }
|
||||||
else edit(2)
|
}
|
||||||
}
|
} catch (err) { if (visible && request == generation) error.value = err instanceof Error ? err.message : '加载失败,请重试' }
|
||||||
|
finally { if (request == generation) loading.value = false }
|
||||||
|
}
|
||||||
|
const loadMore = async () => {
|
||||||
|
if (loading.value || !hasMore.value) return
|
||||||
|
const session = getSession()
|
||||||
|
if (session == null) { refresh(); return }
|
||||||
|
const owner = session['token'] as string
|
||||||
|
const request = ++generation
|
||||||
|
loading.value = true; error.value = ''
|
||||||
|
try {
|
||||||
|
const result = await fetchAssessments(cursor)
|
||||||
|
if (visible && request == generation && sameOwner(owner)) {
|
||||||
|
const incoming = result.items.filter((item: AssessmentRecord): boolean => !records.value.some((old: AssessmentRecord): boolean => old.id == item.id))
|
||||||
|
records.value = records.value.concat(incoming); hasMore.value = result.hasMore; cursor = result.nextCursor
|
||||||
|
}
|
||||||
|
} catch (err) { if (visible && request == generation) error.value = err instanceof Error ? err.message : '加载失败,请重试' }
|
||||||
|
finally { if (request == generation) loading.value = false }
|
||||||
|
}
|
||||||
|
const start = (type: string) => { clearAssessmentFlow(); uni.navigateTo({ url: '/packages/finance/assessment/assessment?type=' + type }) }
|
||||||
|
const openRecord = (id: string) => { uni.navigateTo({ url: '/packages/finance/archive/archive?assessmentId=' + encodeURIComponent(id) }) }
|
||||||
|
const openAnalysis = () => {
|
||||||
|
const page = detail.value?.record.status == 'succeeded' ? 'report/report' : 'analyzing/analyzing'
|
||||||
|
uni.navigateTo({ url: '/packages/finance/' + page + '?assessmentId=' + encodeURIComponent(selectedId.value) })
|
||||||
|
}
|
||||||
|
const backToList = () => {
|
||||||
|
const pages = getCurrentPages()
|
||||||
|
if (pages.length > 1 && pages[pages.length - 2].route == 'packages/finance/archive/archive') uni.navigateBack()
|
||||||
|
else uni.redirectTo({ url: '/packages/finance/archive/archive' })
|
||||||
|
}
|
||||||
|
onBackPress((event): boolean => { if (event.from == 'navigateBack' || selectedId.value.length == 0) return false; backToList(); return true })
|
||||||
|
onLoad((options) => { if (options != null) selectedId.value = options['assessmentId'] ?? '' })
|
||||||
|
onShow(() => { visible = true; refresh() })
|
||||||
|
// 离开时清空敏感详情;迟到的请求不能把其他账户的数据写回页面。
|
||||||
|
onHide(() => { visible = false; generation++; records.value = []; detail.value = null })
|
||||||
|
onUnload(() => { visible = false; generation++ })
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.archive-content { padding: 26rpx 30rpx 64rpx; }
|
.archive-content { margin: 28rpx; padding: 30rpx; border-radius: 18rpx; background-color: #fff; }
|
||||||
.archive-switch { flex-direction: row; background-color: #eaf0f8; border-radius: 16rpx; padding: 6rpx; margin-bottom: 24rpx; }
|
.archive-title { font-size: 32rpx; font-weight: 600; color: #23344e; }
|
||||||
.switch-item { flex: 1; text-align: center; font-size: 27rpx; color: #708099; padding: 18rpx 0; border-radius: 12rpx; }
|
.archive-note { margin: 24rpx 0 30rpx; color: #738198; font-size: 26rpx; line-height: 42rpx; }
|
||||||
.switch-active { background-color: #fff; color: #0868f7; font-weight: 600; }
|
.start-actions { flex-direction: row; margin-bottom: 20rpx; }
|
||||||
.completion { padding: 30rpx; border-radius: 20rpx; background-color: #edf5ff; }
|
.start-button { flex: 1; margin: 6rpx; font-size: 27rpx; }
|
||||||
.completion-head { flex-direction: row; justify-content: space-between; align-items: center; }
|
.record-card { padding: 28rpx 0; border-bottom: 1rpx solid #edf1f6; }
|
||||||
.completion-title { font-size: 30rpx; font-weight: 600; color: #233d64; }
|
.record-title { font-size: 30rpx; font-weight: 600; color: #23344e; }
|
||||||
.completion-score { color: #0868f7; font-size: 52rpx; line-height: 64rpx; font-weight: 800; }
|
.record-status { margin-top: 16rpx; color: #0868f7; font-size: 27rpx; }
|
||||||
.score-unit { color: #0868f7; font-size: 28rpx; font-weight: 600; }
|
.record-time { margin-top: 14rpx; color: #738198; font-size: 23rpx; line-height: 36rpx; }
|
||||||
.completion-desc { color: #6c819d; font-size: 25rpx; line-height: 38rpx; margin-top: 20rpx; }
|
.action { color: #0868f7; font-size: 27rpx; padding: 24rpx 0; }
|
||||||
.progress-track { height: 12rpx; border-radius: 6rpx; background-color: #d9e6f8; margin-top: 20rpx; overflow: hidden; }
|
.notice { color: #ad432d; font-size: 27rpx; line-height: 40rpx; }
|
||||||
.progress-bar { height: 12rpx; border-radius: 6rpx; background-color: #0868f7; }
|
.empty-state { padding: 50rpx 0; color: #738198; font-size: 28rpx; }
|
||||||
.archive-list { margin-top: 24rpx; background-color: #fff; border: 1rpx solid #e5ecf5; border-radius: 18rpx; padding: 0 22rpx; }
|
.detail-row { flex-direction: row; padding: 24rpx 0; border-bottom: 1rpx solid #edf1f6; }
|
||||||
.archive-row { min-height: 126rpx; flex-direction: row; align-items: center; border-bottom: 1rpx solid #edf1f6; }
|
.field-label { width: 210rpx; font-size: 26rpx; color: #738198; }
|
||||||
.archive-labels { flex: 1; min-width: 0; margin: 0 18rpx; }
|
.field-value { flex: 1; text-align: right; font-size: 27rpx; color: #23344e; }
|
||||||
.archive-name { font-size: 28rpx; color: #26374e; font-weight: 500; }
|
|
||||||
.archive-note { font-size: 23rpx; color: #8190a4; margin-top: 8rpx; line-height: 34rpx; }
|
|
||||||
.state { font-size: 23rpx; color: #8c99ab; margin-right: 10rpx; flex-shrink: 0; }
|
|
||||||
.state-done { color: #16a478; }
|
|
||||||
.complete-button { margin-top: 32rpx; }
|
|
||||||
.local-note { font-size: 23rpx; color: #8795a8; line-height: 36rpx; margin-top: 22rpx; text-align: center; }
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<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">
|
||||||
<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 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">
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
<view v-else-if="step == 1" class="form-list">
|
<view v-else-if="step == 1" class="form-list">
|
||||||
<template v-if="assessmentType == 'enterprise'">
|
<template v-if="assessmentType == 'enterprise'">
|
||||||
<AssessmentSelect label="经营行业" v-model="form.occupation" :options="['商贸零售', '制造业', '科技服务', '餐饮住宿', '专业服务', '其他行业']" />
|
<AssessmentSelect label="经营行业" v-model="form.occupation" :options="['商贸零售', '制造业', '科技服务', '餐饮住宿', '专业服务', '其他行业']" />
|
||||||
<AssessmentSelect label="经营年限" v-model="form.operatingYears" :options="['不足 1 年', '1–3 年', '3–5 年', '5 年以上']" />
|
<view class="form-row"><text class="field-label">经营月数</text><input v-model="form.operatingMonths" class="field-input" type="number" maxlength="4" placeholder="请输入实际经营月数" /></view>
|
||||||
<view class="form-row"><text class="field-label">年营收(万元)</text><input v-model="form.annualRevenue" class="field-input" type="digit" placeholder="请输入年营业收入" /></view>
|
<view class="form-row"><text class="field-label">年营收(万元)</text><input v-model="form.annualRevenue" class="field-input" type="digit" placeholder="请输入年营业收入" /></view>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
@@ -29,6 +29,14 @@
|
|||||||
<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>
|
||||||
@@ -58,41 +66,65 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="uts">
|
<script setup lang="uts">
|
||||||
import { getUserStorage, removeUserStorage } from '@/services/storage.uts'
|
import { getSession } from '@/services/api.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'
|
||||||
import AssessmentSelect from '@/packages/finance/components/AssessmentSelect.uvue'
|
import AssessmentSelect from '@/packages/finance/components/AssessmentSelect.uvue'
|
||||||
import { AssessmentForm, SelectedDocument, DataAuthorization, emptyAssessmentForm, activeAssessmentType, loadAssessmentDraft, formFromDraft, documentsFromDraft, saveAssessmentDraft, showAgreementNotice, assetValidationMessage } 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 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
|
||||||
|
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 saveDraft = () => {
|
const rememberInput = () => {
|
||||||
if (!initialized) return
|
if (!initialized || !sameOwner()) return
|
||||||
const source = form.value
|
const source = form.value
|
||||||
const draftForm: 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 }
|
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 }
|
||||||
if (draftForm.city == '其他城市' && customCity.value.trim().length > 0) draftForm.city = customCity.value.trim()
|
if (currentForm.city == '其他城市' && customCity.value.trim().length > 0) currentForm.city = customCity.value.trim()
|
||||||
saveAssessmentDraft(assessmentType.value, step.value, draftForm, documents.value, authorizations.value, documentConsent.value, dataConsent.value)
|
setAssessmentInput(assessmentType.value, step.value, currentForm, documents.value, authorizations.value, documentConsent.value, dataConsent.value)
|
||||||
}
|
}
|
||||||
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
|
||||||
authorizations.value.forEach((item) => { item.selected = false })
|
authorizations.value.forEach((item) => { item.selected = false })
|
||||||
const saved = loadAssessmentDraft(type)
|
const saved = getAssessmentInput(type)
|
||||||
if (saved != null) {
|
if (saved != null) {
|
||||||
form.value = formFromDraft(saved); documents.value = documentsFromDraft(saved)
|
form.value = formFromInput(saved); documents.value = documentsFromInput(saved)
|
||||||
const savedStep = saved['step']; if (typeof savedStep == 'number') step.value = Math.max(0, Math.min(3, savedStep as number))
|
const savedStep = saved['step']; if (typeof savedStep == 'number') step.value = Math.max(0, Math.min(3, savedStep as number))
|
||||||
documentConsent.value = saved['documentConsent'] == true; dataConsent.value = saved['dataConsent'] == true
|
documentConsent.value = saved['documentConsent'] == true; dataConsent.value = saved['dataConsent'] == true
|
||||||
const grants = saved['authorizations'] as string[] | null
|
const grants = saved['authorizations'] as string[] | null
|
||||||
@@ -100,27 +132,41 @@
|
|||||||
if (form.value.city.length > 0 && !['深圳市', '广州市', '北京市', '上海市', '杭州市', '其他城市'].includes(form.value.city)) { customCity.value = form.value.city; form.value.city = '其他城市' }
|
if (form.value.city.length > 0 && !['深圳市', '广州市', '北京市', '上海市', '杭州市', '其他城市'].includes(form.value.city)) { customCity.value = form.value.city; form.value.city = '其他城市' }
|
||||||
}
|
}
|
||||||
initialized = true
|
initialized = true
|
||||||
saveDraft()
|
rememberInput()
|
||||||
}
|
}
|
||||||
const switchType = (type: string) => { if (assessmentType.value == type) return; saveDraft(); restore(type); step.value = 0; saveDraft() }
|
const switchType = (type: string) => { if (assessmentType.value == type) return; rememberInput(); restore(type); step.value = 0; rememberInput() }
|
||||||
onLoad((options) => {
|
onLoad((options) => {
|
||||||
|
const session = getSession()
|
||||||
|
if (session == null) { uni.redirectTo({ url: '/packages/account/login/login' }); return }
|
||||||
|
ownerToken = session['token'] as string
|
||||||
|
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)
|
restore(type)
|
||||||
|
loadPurposes()
|
||||||
})
|
})
|
||||||
onShow(() => {
|
onShow(() => {
|
||||||
const requestedStep = getUserStorage('assessmentEditStep')
|
goingToConfirm = false
|
||||||
if (typeof requestedStep == 'number') { step.value = Math.max(0, Math.min(3, requestedStep as number)); removeUserStorage('assessmentEditStep'); saveDraft() }
|
if (initialized && !sameOwner()) { initialized = false; form.value = emptyAssessmentForm(); clearAssessmentFlow(); uni.redirectTo({ url: '/packages/account/login/login' }) }
|
||||||
})
|
const input = getAssessmentInput(assessmentType.value)
|
||||||
onHide(() => saveDraft())
|
if (initialized && input != null && typeof input['step'] == 'number') step.value = Math.max(0, Math.min(3, input['step'] as number))
|
||||||
onUnload(() => saveDraft())
|
})
|
||||||
// 系统返回与顶部返回均先保存;页内返回优先回到上一步。
|
// 离开填写流程即丢弃未提交内容;进入确认页时只在内存传递。
|
||||||
onBackPress((): boolean => { saveDraft(); if (step.value > 0) { step.value--; saveDraft(); return true }; return false })
|
onHide(() => {
|
||||||
const previous = () => { saveDraft(); if (step.value > 0) step.value--; saveDraft() }
|
if (!goingToConfirm) {
|
||||||
const back = () => { if (step.value > 0) { previous(); return }; saveDraft(); if (getCurrentPages().length > 1) uni.navigateBack(); else uni.redirectTo({ url: '/packages/finance/entry/entry?type=' + assessmentType.value }) }
|
clearAssessmentFlow(); form.value = emptyAssessmentForm(); step.value = 0; documents.value = []
|
||||||
|
customCity.value = ''; documentConsent.value = false; dataConsent.value = false
|
||||||
|
authorizations.value.forEach((item) => { item.selected = false })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
onUnload(() => { initialized = false; clearAssessmentFlow() })
|
||||||
|
// 页内前后步骤只维护当前内存状态,返回入口后不保留填写内容。
|
||||||
|
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 back = () => { if (step.value > 0) { previous(); return }; rememberInput(); if (getCurrentPages().length > 1) uni.navigateBack(); else uni.redirectTo({ url: '/packages/finance/entry/entry?type=' + assessmentType.value }) }
|
||||||
const documentPath = (name: string): string => { const item = documents.value.find((file) => file.name == name); return item == null ? '' : item.path }
|
const documentPath = (name: string): string => { const item = documents.value.find((file) => file.name == name); return item == null ? '' : item.path }
|
||||||
const documentExpired = (name: string) => {
|
const documentExpired = (name: string) => {
|
||||||
documents.value = documents.value.filter((file) => file.name != name); saveDraft()
|
documents.value = documents.value.filter((file) => file.name != name); rememberInput()
|
||||||
uni.showToast({ title: name + '已失效,请重新选择', icon: 'none' })
|
uni.showToast({ title: name + '已失效,请重新选择', icon: 'none' })
|
||||||
}
|
}
|
||||||
const chooseDocument = (name: string) => {
|
const chooseDocument = (name: string) => {
|
||||||
@@ -128,7 +174,7 @@
|
|||||||
if (result.tempFilePaths.length == 0) return
|
if (result.tempFilePaths.length == 0) return
|
||||||
documents.value = documents.value.filter((file) => file.name != name)
|
documents.value = documents.value.filter((file) => file.name != name)
|
||||||
const document: SelectedDocument = { name, path: result.tempFilePaths[0] }
|
const document: SelectedDocument = { name, path: result.tempFilePaths[0] }
|
||||||
documents.value.push(document); saveDraft()
|
documents.value.push(document); rememberInput()
|
||||||
}, fail: (error) => { if (!error.errMsg.includes('cancel')) uni.showToast({ title: '选择失败,请重试或检查相册权限', icon: 'none' }) } })
|
}, fail: (error) => { if (!error.errMsg.includes('cancel')) uni.showToast({ title: '选择失败,请重试或检查相册权限', icon: 'none' }) } })
|
||||||
}
|
}
|
||||||
const documentAction = (name: string) => {
|
const documentAction = (name: string) => {
|
||||||
@@ -137,7 +183,7 @@
|
|||||||
uni.showActionSheet({ itemList: ['预览图片', '重新选择', '移除资料'], success: (result) => {
|
uni.showActionSheet({ itemList: ['预览图片', '重新选择', '移除资料'], success: (result) => {
|
||||||
if (result.tapIndex == 0) uni.previewImage({ urls: [path], fail: () => { uni.showToast({ title: '本地图片已失效,请重新选择', icon: 'none' }) } })
|
if (result.tapIndex == 0) uni.previewImage({ urls: [path], fail: () => { uni.showToast({ title: '本地图片已失效,请重新选择', icon: 'none' }) } })
|
||||||
else if (result.tapIndex == 1) chooseDocument(name)
|
else if (result.tapIndex == 1) chooseDocument(name)
|
||||||
else if (result.tapIndex == 2) { documents.value = documents.value.filter((file) => file.name != name); saveDraft() }
|
else if (result.tapIndex == 2) { documents.value = documents.value.filter((file) => file.name != name); rememberInput() }
|
||||||
} })
|
} })
|
||||||
}
|
}
|
||||||
const validateStep = (): boolean => {
|
const validateStep = (): boolean => {
|
||||||
@@ -153,6 +199,9 @@
|
|||||||
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 = '请阅读并同意资料使用协议,或移除资料'
|
||||||
@@ -160,7 +209,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++; saveDraft() } else { saveDraft(); uni.navigateTo({ url: '/packages/finance/confirm/confirm' }) } }
|
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 } }) } }
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@@ -182,6 +231,9 @@
|
|||||||
.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; }
|
||||||
|
|||||||
@@ -5,29 +5,29 @@
|
|||||||
<view class="confirm-content">
|
<view class="confirm-content">
|
||||||
<view class="confirm-progress"><view class="progress-line"><view class="progress-segment" v-for="i in 5" :key="i"></view></view><text class="progress-count">5/5</text></view>
|
<view class="confirm-progress"><view class="progress-line"><view class="progress-segment" v-for="i in 5" :key="i"></view></view><text class="progress-count">5/5</text></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>
|
<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="!hasDraft" 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>
|
||||||
<view class="submit-bar" :style="bottomStyle(20)"><button class="primary-button submit-button" :disabled="submitting" @click="submit">{{ !hasDraft ? '去填写信息' : (submitting ? '提交中…' : '确认并开始评估') }}</button></view>
|
<view class="submit-bar" :style="bottomStyle(20)"><button class="primary-button submit-button" :disabled="submitting" @click="submit">{{ !hasInput ? '去填写信息' : (submitting ? '提交中…' : '确认提交资料') }}</button></view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="uts">
|
<script setup lang="uts">
|
||||||
import { setUserStorage } from '@/services/storage.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'
|
||||||
import { activeAssessmentType, loadAssessmentDraft, draftText, documentsFromDraft, ownershipFromDraft, formFromDraft, assetValidationMessage } from '@/services/assessment.uts'
|
import { activeAssessmentType, getAssessmentInput, draftText, documentsFromInput, ownershipFromInput, formFromInput, assetValidationMessage, clearAssessmentFlow } from '@/services/assessment.uts'
|
||||||
const { pageStyle, contentStyle, bottomStyle } = usePageLayout()
|
const { pageStyle, contentStyle, bottomStyle } = usePageLayout()
|
||||||
type InfoRow = { label: string, value: string }
|
type InfoRow = { label: string, value: string }
|
||||||
type InfoSection = { title: string, step: number, rows: InfoRow[], expanded: boolean }
|
type InfoSection = { title: string, step: number, rows: InfoRow[], expanded: boolean }
|
||||||
const submitting = ref(false)
|
const submitting = ref(false)
|
||||||
const hasDraft = ref(false)
|
const hasInput = ref(false)
|
||||||
const assessmentType = ref('personal')
|
const assessmentType = ref('personal')
|
||||||
const sections = ref<InfoSection[]>([])
|
const sections = ref<InfoSection[]>([])
|
||||||
const payload = ref<UTSJSONObject>({ source: 'mobile', confirmed: false })
|
const payload = ref<UTSJSONObject>({ source: 'mobile', confirmed: false })
|
||||||
@@ -35,11 +35,11 @@
|
|||||||
const displayValue = (saved: UTSJSONObject, key: string): string => { const value = draftText(saved, key); return value.length > 0 ? value : '未填写' }
|
const displayValue = (saved: UTSJSONObject, key: string): string => { const value = draftText(saved, key); return value.length > 0 ? value : '未填写' }
|
||||||
const appendRow = (rows: InfoRow[], label: string, value: string) => { const row: InfoRow = { label, value }; rows.push(row) }
|
const appendRow = (rows: InfoRow[], label: string, value: string) => { const row: InfoRow = { label, value }; rows.push(row) }
|
||||||
const valueWithUnit = (saved: UTSJSONObject, key: string, unit: string): string => { const value = draftText(saved, key); return value.length > 0 ? value + ' ' + unit : '未填写' }
|
const valueWithUnit = (saved: UTSJSONObject, key: string, unit: string): string => { const value = draftText(saved, key); return value.length > 0 ? value + ' ' + unit : '未填写' }
|
||||||
const loadDraft = () => {
|
const loadInput = () => {
|
||||||
assessmentType.value = activeAssessmentType()
|
assessmentType.value = activeAssessmentType()
|
||||||
const saved = loadAssessmentDraft(assessmentType.value)
|
const saved = getAssessmentInput(assessmentType.value)
|
||||||
if (saved == null) { hasDraft.value = false; sections.value = []; return }
|
if (saved == null) { hasInput.value = false; sections.value = []; return }
|
||||||
hasDraft.value = true
|
hasInput.value = true
|
||||||
const enterprise = assessmentType.value == 'enterprise'
|
const enterprise = assessmentType.value == 'enterprise'
|
||||||
const basicRows: InfoRow[] = []
|
const basicRows: InfoRow[] = []
|
||||||
if (enterprise) {
|
if (enterprise) {
|
||||||
@@ -54,11 +54,17 @@
|
|||||||
appendRow(basicRows, '婚姻状况', displayValue(saved, 'maritalStatus'))
|
appendRow(basicRows, '婚姻状况', displayValue(saved, 'maritalStatus'))
|
||||||
}
|
}
|
||||||
const incomeRows: InfoRow[] = enterprise ? [
|
const incomeRows: InfoRow[] = enterprise ? [
|
||||||
{ label: '经营行业', value: displayValue(saved, 'occupation') }, { label: '经营年限', value: displayValue(saved, 'operatingYears') }, { label: '年营业收入', value: draftText(saved, 'annualRevenue').length > 0 ? draftText(saved, 'annualRevenue') + ' 万元' : '未填写' }
|
{ label: '经营行业', value: displayValue(saved, 'occupation') }, { label: '经营月数', value: valueWithUnit(saved, 'operatingMonths', '个月') }, { label: '年营业收入', value: draftText(saved, 'annualRevenue').length > 0 ? draftText(saved, 'annualRevenue') + ' 万元' : '未填写' }
|
||||||
] : [
|
] : [
|
||||||
{ 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 = documentsFromDraft(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[] = []
|
||||||
@@ -66,6 +72,7 @@
|
|||||||
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', '万元') },
|
||||||
@@ -73,22 +80,27 @@
|
|||||||
{ label: '银行信用卡张数', value: valueWithUnit(saved, 'creditCardCount', '张') },
|
{ label: '银行信用卡张数', value: valueWithUnit(saved, 'creditCardCount', '张') },
|
||||||
{ label: '总额度', value: valueWithUnit(saved, 'creditCardTotalLimit', '万元') },
|
{ label: '总额度', value: valueWithUnit(saved, 'creditCardTotalLimit', '万元') },
|
||||||
{ label: '已使用总额度', value: valueWithUnit(saved, 'creditCardUsedLimit', '万元') },
|
{ label: '已使用总额度', value: valueWithUnit(saved, 'creditCardUsedLimit', '万元') },
|
||||||
{ label: '是否有房', value: ownershipFromDraft(saved, 'house').length > 0 ? ownershipFromDraft(saved, 'house') : '未填写' },
|
{ label: '是否有房', value: ownershipFromInput(saved, 'house').length > 0 ? ownershipFromInput(saved, 'house') : '未填写' },
|
||||||
{ label: '是否有车', value: ownershipFromDraft(saved, 'car').length > 0 ? ownershipFromDraft(saved, 'car') : '未填写' }
|
{ label: '是否有车', value: ownershipFromInput(saved, 'car').length > 0 ? ownershipFromInput(saved, 'car') : '未填写' }
|
||||||
], expanded: false },
|
], expanded: false },
|
||||||
{ title: '资料与授权', step: 3, rows: [{ label: '本地资料', value: fileNames.length > 0 ? fileNames : '未选择' }, { label: '授权意向', value: grantNames.length > 0 ? grantNames.join('、') : '未选择' }, { label: '当前状态', value: '资料未上传 · 数据未查询' }], expanded: false }
|
{ title: '资料与授权', step: 3, rows: [{ label: '本地资料', value: fileNames.length > 0 ? fileNames : '未选择' }, { label: '授权意向', value: grantNames.length > 0 ? grantNames.join('、') : '未选择' }, { label: '当前状态', value: '资料未上传 · 数据未查询' }], expanded: false }
|
||||||
]
|
]
|
||||||
payload.value = { ...saved, source: 'mobile', confirmed: true }
|
payload.value = { ...saved, source: 'mobile', confirmed: true }
|
||||||
}
|
}
|
||||||
onShow(() => loadDraft())
|
onShow(() => loadInput())
|
||||||
|
onUnload(() => {
|
||||||
|
const pages = getCurrentPages()
|
||||||
|
if (!pages.some((page): boolean => page.route == 'packages/finance/assessment/assessment')) clearAssessmentFlow()
|
||||||
|
})
|
||||||
const edit = (step: number) => {
|
const edit = (step: number) => {
|
||||||
setUserStorage('assessmentEditStep', step)
|
const input = getAssessmentInput(assessmentType.value)
|
||||||
|
if (input != null) input['step'] = step
|
||||||
const pages = getCurrentPages()
|
const pages = getCurrentPages()
|
||||||
if (pages.length > 1 && pages[pages.length - 2].route == 'packages/finance/assessment/assessment') uni.navigateBack({ delta: 1 })
|
if (pages.length > 1 && pages[pages.length - 2].route == 'packages/finance/assessment/assessment') uni.navigateBack({ delta: 1 })
|
||||||
else uni.navigateTo({ url: '/packages/finance/assessment/assessment?type=' + assessmentType.value })
|
else uni.navigateTo({ url: '/packages/finance/assessment/assessment?type=' + assessmentType.value })
|
||||||
}
|
}
|
||||||
const submit = async () => {
|
const submit = async () => {
|
||||||
if (!hasDraft.value) { edit(0); return }
|
if (!hasInput.value) { edit(0); return }
|
||||||
if (submitting.value) return
|
if (submitting.value) return
|
||||||
const saved = payload.value
|
const saved = payload.value
|
||||||
const amount = draftText(saved, assessmentType.value == 'enterprise' ? 'annualRevenue' : 'monthlyIncome')
|
const amount = draftText(saved, assessmentType.value == 'enterprise' ? 'annualRevenue' : 'monthlyIncome')
|
||||||
@@ -96,12 +108,19 @@
|
|||||||
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 ((documentsFromDraft(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(formFromDraft(saved))
|
const assetError = assetValidationMessage(formFromInput(saved))
|
||||||
if (assetError.length > 0) { uni.showToast({ title: assetError, icon: 'none' }); edit(2); return }
|
if (assetError.length > 0) { uni.showToast({ title: assetError, icon: 'none' }); edit(2); return }
|
||||||
submitting.value = true
|
submitting.value = true
|
||||||
try { await submitAssessment(payload.value); uni.redirectTo({ url: '/packages/finance/analyzing/analyzing' }) }
|
try {
|
||||||
|
const id = await submitAssessment(payload.value)
|
||||||
|
clearAssessmentFlow()
|
||||||
|
// 已提交记录只读;清空页面栈,防止返回旧表单再次提交。
|
||||||
|
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' }) }
|
||||||
finally { submitting.value = false }
|
finally { submitting.value = false }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,47 +1,68 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="page" :style="pageStyle()">
|
<view class="page" :style="pageStyle()">
|
||||||
<AppHeader title="融资需求报告" />
|
<AppHeader title="融资资料分析报告" />
|
||||||
<scroll-view scroll-y class="report-scroll" :style="contentStyle(96, false)">
|
<scroll-view scroll-y :style="contentStyle(0, false)">
|
||||||
<view class="report-content">
|
<view class="report-content">
|
||||||
<view class="score-panel"><text class="score-label">综合评分</text><view class="score-row"><text class="big-score">85</text><text class="score-unit">分</text><text class="score-badge">良好</text></view><text class="score-caption">从五个维度了解融资资质</text></view>
|
<text class="report-note">评估记录 · {{ assessmentId }}</text>
|
||||||
<view class="radar-panel"><image class="radar-image" src="/packages/finance/static/finance-radar.png" mode="aspectFit"></image><text class="radar-label top">还款能力</text><text class="radar-label left">收入稳定</text><text class="radar-label right">信用状况</text><text class="radar-label bottom-left">资产条件</text><text class="radar-label bottom-right">负债情况</text></view>
|
<text v-if="loading" class="body-text">正在加载报告…</text>
|
||||||
<view class="conclusion"><text class="section-title">评估结论</text><text class="conclusion-text">可进一步比较信用融资产品的额度、期限与还款安排,结合自身需求选择合适方案。</text><view class="suggest-row"><text class="suggest-label">建议期限</text><text class="suggest-value">6–36个月</text></view><text class="report-note">报告内容仅供参考,实际融资结果以授权资料和机构审核为准。</text></view>
|
<view v-if="error.length > 0" class="panel"><text class="body-text">{{ error }}</text><text class="action" @click="loadReport">重新加载</text><text class="action" @click="history">前往融资档案</text></view>
|
||||||
<button class="primary-button" @click="viewMatches">查看匹配方案</button>
|
<template v-if="report != null">
|
||||||
</view>
|
<text class="report-note">分析模型:{{ report!.modelCode }}</text>
|
||||||
</scroll-view>
|
<template v-if="report!.version == 2">
|
||||||
</view>
|
<view 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"><text class="section-title">资料完整度</text><text class="readiness-value">{{ report!.completenessText }}</text><text class="report-note">仅统计当前适用的自填字段,不代表信用评分或真实性。</text><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></view>
|
||||||
|
<view class="panel"><text class="section-title">融资准备状态</text><text class="readiness-value">{{ report!.readinessLabel }}</text><text class="body-text" v-for="(item, index) in report!.readinessBasis" :key="index">{{ item }}</text><text class="report-note">资料未经核验;即使完整度达到100%,仍需核对机构要求的材料。</text></view>
|
||||||
|
</template>
|
||||||
|
<view class="panel"><text class="section-title">自填资料分析</text><text class="body-text">{{ report!.summary }}</text><text class="report-note">{{ report!.disclaimer }}</text></view>
|
||||||
|
<view class="panel"><text class="section-title">本次提交的事实</text><text class="report-note">来源:用户自填,未经外部核验;未填写不等于零。</text><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></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 v-if="report!.suggestions.length > 0" class="panel"><text class="section-title">完善建议</text><text class="body-text" v-for="(item, index) in report!.suggestions" :key="index">{{ item }}</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></view>
|
||||||
|
<view class="panel"><text class="section-title">产品匹配</text><text class="body-text">{{ report!.matchingMessage }}</text></view>
|
||||||
|
<text class="action" @click="history">查看历史评估</text>
|
||||||
|
</template>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="uts">
|
<script setup lang="uts">
|
||||||
import { usePageLayout } from '@/services/layout.uts'
|
import AppHeader from '@/components/AppHeader.uvue'
|
||||||
const { contentStyle, pageStyle } = usePageLayout()
|
import { usePageLayout } from '@/services/layout.uts'
|
||||||
import AppHeader from '@/components/AppHeader.uvue'
|
import { getSession } from '@/services/api.uts'
|
||||||
const viewMatches = () => { uni.navigateTo({ url: '/packages/finance/matches/matches' }) }
|
import { AnalysisReport, fetchAnalysisReport } from '@/packages/finance/services/records.uts'
|
||||||
|
const { contentStyle, pageStyle } = usePageLayout()
|
||||||
|
const assessmentId = ref(''); const loading = ref(false); const error = ref('')
|
||||||
|
const report = ref<AnalysisReport | null>(null)
|
||||||
|
let generation = 0; let visible = false
|
||||||
|
const loadReport = async () => {
|
||||||
|
const request = ++generation
|
||||||
|
report.value = null; error.value = ''
|
||||||
|
if (assessmentId.value.length == 0) { error.value = '请从融资档案选择已完成的评估记录。'; return }
|
||||||
|
const session = getSession()
|
||||||
|
if (session == null) { uni.redirectTo({ url: '/packages/account/login/login' }); return }
|
||||||
|
const owner = session['token'] as string
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const result = await fetchAnalysisReport(assessmentId.value)
|
||||||
|
if (visible && request == generation && getSession()?.getString('token') == owner) report.value = result
|
||||||
|
} catch (err) { if (visible && request == generation) error.value = err instanceof Error ? err.message : '报告加载失败,请重试' }
|
||||||
|
finally { if (request == generation) loading.value = false }
|
||||||
|
}
|
||||||
|
const history = () => { uni.navigateTo({ url: '/packages/finance/archive/archive' }) }
|
||||||
|
onLoad((options) => { if (options != null) assessmentId.value = options['assessmentId'] ?? '' })
|
||||||
|
onShow(() => { visible = true; loadReport() })
|
||||||
|
// 报告只留当前可见页面,账户切换或离开后不能由迟到请求恢复旧资料。
|
||||||
|
onHide(() => { visible = false; generation++; report.value = null; error.value = '' })
|
||||||
|
onUnload(() => { visible = false; generation++ })
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.report-scroll { }
|
.report-content { padding: 28rpx 30rpx 80rpx; }
|
||||||
.report-content { padding: 28rpx 30rpx 80rpx; }
|
.panel { margin-top: 24rpx; padding: 28rpx; border-radius: 18rpx; background-color: #fff; border: 1rpx solid #e7edf6; }
|
||||||
.score-panel { background-color: #fff; border-radius: 18rpx; padding: 30rpx; border: 1rpx solid #e7edf6; }
|
.body-text { margin-top: 20rpx; font-size: 27rpx; color: #42536b; line-height: 44rpx; }
|
||||||
.score-label { font-size: 24rpx; color: #6f7d91; }
|
.report-note { font-size: 23rpx; line-height: 38rpx; color: #78889e; margin-top: 18rpx; }
|
||||||
.score-row { flex-direction: row; align-items: flex-end; margin-top: 8rpx; }
|
.fact-row { flex-direction: row; padding: 22rpx 0; border-bottom: 1rpx solid #edf1f6; }
|
||||||
.big-score { color: #102557; font-size: 82rpx; line-height: 90rpx; font-weight: 900; }
|
.fact-label { flex: 1; font-size: 25rpx; color: #738198; }
|
||||||
.score-unit { font-size: 28rpx; margin: 0 16rpx 14rpx 5rpx; }
|
.fact-value { flex: 1; text-align: right; font-size: 26rpx; color: #23344e; }
|
||||||
.score-badge { padding: 8rpx 17rpx; border-radius: 10rpx; background-color: #e5f8ee; color: #0ba868; font-size: 22rpx; margin-bottom: 15rpx; }
|
.action { padding: 26rpx 0; color: #0868f7; font-size: 27rpx; }
|
||||||
.score-caption { color: #8793a6; font-size: 22rpx; margin-top: 6rpx; }
|
.readiness-value { margin-top: 20rpx; font-size: 32rpx; line-height: 48rpx; font-weight: bold; color: #23344e; }
|
||||||
.radar-panel { height: 370rpx; margin-top: 20rpx; border-radius: 18rpx; background-color: #fff; border: 1rpx solid #e7edf6; align-items: center; justify-content: center; position: relative; overflow: hidden; }
|
|
||||||
.radar-image { width: 340rpx; height: 310rpx; }
|
|
||||||
.radar-core { position: absolute; width: 92rpx; height: 92rpx; left: 50%; top: 50%; margin-left: -46rpx; margin-top: -46rpx; border-radius: 50%; background-color: #0868f7; align-items: center; justify-content: center; color: white; font-size: 34rpx; font-weight: 800; box-shadow: 0 0 32rpx rgba(8,104,247,.28); }
|
|
||||||
.radar-label { position: absolute; color: #58667b; font-size: 21rpx; }
|
|
||||||
.top { top: 28rpx; }
|
|
||||||
.left { left: 27rpx; top: 162rpx; }
|
|
||||||
.right { right: 27rpx; top: 162rpx; }
|
|
||||||
.bottom-left { left: 95rpx; bottom: 32rpx; }
|
|
||||||
.bottom-right { right: 86rpx; bottom: 32rpx; }
|
|
||||||
.conclusion { margin: 24rpx 0 32rpx; background-color: #fff; border-radius: 18rpx; padding: 28rpx; border: 1rpx solid #e7edf6; }
|
|
||||||
.conclusion-text { margin-top: 18rpx; font-size: 24rpx; color: #59677b; line-height: 42rpx; }
|
|
||||||
.suggest-row { margin-top: 24rpx; padding-top: 22rpx; border-top: 1rpx solid #edf1f6; flex-direction: row; justify-content: space-between; font-size: 25rpx; }
|
|
||||||
.suggest-value { color: #0868f7; font-size: 27rpx; font-weight: 700; }
|
|
||||||
.suggest-label { color: #53667f; font-size: 25rpx; }
|
|
||||||
.report-note { font-size: 21rpx; line-height: 34rpx; color: #78889e; margin-top: 20rpx; }
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,20 +1,10 @@
|
|||||||
import { setUserStorage } from '@/services/storage.uts'
|
|
||||||
import { API_DEMO_ENABLED, API_BASE_URL, wait, requestJson } from '@/services/api.uts'
|
import { API_DEMO_ENABLED, API_BASE_URL, wait, requestJson } from '@/services/api.uts'
|
||||||
import { demoProducts, parseProduct, FinanceProduct } from './finance.uts'
|
import { demoProducts, parseProduct, FinanceProduct } from './finance.uts'
|
||||||
import { assessmentApiPayload } from '@/services/assessment.uts'
|
import { createAssessment } from './records.uts'
|
||||||
export { API_BASE_URL } from '@/services/api.uts'
|
export { API_BASE_URL } from '@/services/api.uts'
|
||||||
|
|
||||||
export async function submitAssessment(payload: UTSJSONObject): Promise<string> {
|
export async function submitAssessment(payload: UTSJSONObject): Promise<string> {
|
||||||
const apiPayload = assessmentApiPayload(payload)
|
return await createAssessment(payload)
|
||||||
if (API_DEMO_ENABLED) {
|
|
||||||
await wait(650)
|
|
||||||
setUserStorage('latestAssessment', apiPayload)
|
|
||||||
return 'ASSESSMENT-DEMO-001'
|
|
||||||
}
|
|
||||||
const result = await requestJson('/assessments', 'POST', apiPayload)
|
|
||||||
const id = result['id'] as string | null
|
|
||||||
if (id == null || id.length == 0) throw new Error('评估提交成功但未返回编号')
|
|
||||||
return id
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchMatches(): Promise<FinanceProduct[]> {
|
export async function fetchMatches(): Promise<FinanceProduct[]> {
|
||||||
|
|||||||
@@ -0,0 +1,170 @@
|
|||||||
|
import { getSession, requestJson } from '@/services/api.uts'
|
||||||
|
import { assessmentApiPayload, draftText } from '@/services/assessment.uts'
|
||||||
|
import { assessmentRequestKey } from '@/services/assessment-flow.uts'
|
||||||
|
import { yuanToWan, wanToYuan } from '@/services/money.uts'
|
||||||
|
import { demandValidationMessage } from '@/services/assessment-demand.uts'
|
||||||
|
|
||||||
|
export type AssessmentRecord = { id: string, type: string, status: string, statusLabel: string, message: string, city: string, submittedAt: string, taskId: string, stage: string, canRetry: boolean }
|
||||||
|
export type AssessmentPage = { items: AssessmentRecord[], hasMore: boolean, nextCursor: string }
|
||||||
|
export type AssessmentField = { label: string, value: string }
|
||||||
|
export type AssessmentDetail = { record: AssessmentRecord, fields: AssessmentField[] }
|
||||||
|
|
||||||
|
function recordFrom(item: UTSJSONObject): AssessmentRecord {
|
||||||
|
const submitted = draftText(item, 'submittedAt')
|
||||||
|
const date = new Date(submitted)
|
||||||
|
const pad = (value: number): string => value < 10 ? '0' + value.toString() : value.toString()
|
||||||
|
const dateLabel = submitted.length == 0 || isNaN(date.getTime()) ? '时间未知' : date.getFullYear().toString() + '-' + pad(date.getMonth() + 1) + '-' + pad(date.getDate()) + ' ' + pad(date.getHours()) + ':' + pad(date.getMinutes())
|
||||||
|
return { id: draftText(item, 'assessmentId'), type: draftText(item, 'type'), status: draftText(item, 'status'), statusLabel: draftText(item, 'statusLabel'), message: draftText(item, 'message'), city: draftText(item, 'city'), submittedAt: dateLabel, taskId: draftText(item, 'taskId'), stage: draftText(item, 'stage'), canRetry: item['canRetry'] == true }
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function retryAssessment(id: string, requestId: string): Promise<void> {
|
||||||
|
await requestJson('/assessments/' + encodeURIComponent(id) + '/retry', 'POST', { requestId })
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AnalysisReport = { version: number, summary: string, facts: AssessmentField[], observations: string[], suggestions: string[], dataGaps: string[], disclaimer: string, matchingMessage: string, modelCode: string, demand: AssessmentField[], completenessText: string, completenessItems: AssessmentField[], readinessLabel: string, readinessBasis: string[], warnings: string[] }
|
||||||
|
export async function fetchAnalysisReport(id: string): Promise<AnalysisReport> {
|
||||||
|
const result = await requestJson('/assessments/' + encodeURIComponent(id) + '/report', 'GET', {})
|
||||||
|
const version = result['resultVersion'] as number
|
||||||
|
if (result['assessmentId'] != id || (version != 1 && version != 2) || (version == 1 && result['reportType'] != 'self_reported_analysis') || (version == 2 && result['reportType'] != 'financing_needs_analysis')) throw new Error('报告格式不支持')
|
||||||
|
const facts: AssessmentField[] = []
|
||||||
|
const rawFacts = result['facts'] as UTSJSONObject[]
|
||||||
|
rawFacts.forEach((fact: UTSJSONObject) => {
|
||||||
|
const raw = fact['value']
|
||||||
|
let value = raw == null ? '未填写' : (typeof raw == 'number' ? (raw as number).toString() : draftText(fact, 'value'))
|
||||||
|
if (fact['value'] != null && fact['unit'] == 'yuan') value = yuanToWan(value) + ' 万元'
|
||||||
|
if (fact['value'] != null && fact['unit'] == 'month') value += ' 个月'
|
||||||
|
if (fact['state'] == 'invalid') value = '格式需核实'
|
||||||
|
const enumLabels: UTSJSONObject = { six_months_plus: '连续缴纳6个月以上', under_six_months: '缴纳不足6个月', none: '无' }
|
||||||
|
if (enumLabels[value] != null) value = enumLabels[value] as string
|
||||||
|
// v2的需求在独立区块展示,避免用途代码和同一金额重复出现。
|
||||||
|
if (version == 2 && ['requestedAmountYuan', 'requestedTermMonths', 'purposeCode'].includes(draftText(fact, 'fieldKey'))) return
|
||||||
|
facts.push({ label: draftText(fact, 'label'), value } as AssessmentField)
|
||||||
|
})
|
||||||
|
if (version == 2) {
|
||||||
|
const demandData = result['demand'] as UTSJSONObject
|
||||||
|
const completeness = result['completeness'] as UTSJSONObject
|
||||||
|
const readiness = result['readiness'] as UTSJSONObject
|
||||||
|
const items = completeness['items'] as UTSJSONObject[]
|
||||||
|
const basis = readiness['basis'] as UTSJSONObject[]
|
||||||
|
const stateLabels: UTSJSONObject = { provided: '已填写', missing: '待补充', invalid: '需核实', not_applicable: '不适用' }
|
||||||
|
const completenessItems = items.map((item: UTSJSONObject): AssessmentField => ({ label: draftText(item, 'label'), value: stateLabels[draftText(item, 'state')] as string }))
|
||||||
|
const demand: AssessmentField[] = [
|
||||||
|
{ label: '融资金额', value: yuanToWan(draftText(demandData, 'requestedAmountYuan')) + ' 万元' },
|
||||||
|
{ label: '融资期限', value: (demandData['requestedTermMonths'] as number).toString() + ' 个月' },
|
||||||
|
{ label: '资金用途', value: draftText(demandData, 'purposeLabel') }
|
||||||
|
]
|
||||||
|
const evaluated = completeness['status'] == 'evaluated'
|
||||||
|
const completenessText = evaluated ? (completeness['percent'] as number).toString() + '% · 已填写 ' + (completeness['providedCount'] as number).toString() + ' / ' + (completeness['applicableCount'] as number).toString() + ' 项' : '资料完整度规则待配置'
|
||||||
|
// 模型文本仅显示已通过后端验证的证据引用;准备状态不由模型推导。
|
||||||
|
const explain = (key: string): string[] => (result[key] as UTSJSONObject[]).map((item: UTSJSONObject): string => {
|
||||||
|
const labels = (item['evidenceRefs'] as string[]).map((ref: string): string => {
|
||||||
|
if (ref.startsWith('fact:')) {
|
||||||
|
const fact = rawFacts.find((entry: UTSJSONObject): boolean => draftText(entry, 'fieldKey') == ref.slice(5))
|
||||||
|
return fact == null ? '' : draftText(fact, 'label')
|
||||||
|
}
|
||||||
|
const entry = basis.find((entry: UTSJSONObject): boolean => draftText(entry, 'basisId') == ref.slice(10))
|
||||||
|
return entry == null ? '' : draftText(entry, 'message')
|
||||||
|
}).filter((label: string): boolean => label.length > 0)
|
||||||
|
return draftText(item, 'text') + (labels.length > 0 ? '(依据:' + labels.join(';') + ')' : '')
|
||||||
|
})
|
||||||
|
return { version, summary: draftText(result, 'summary'), facts, observations: explain('strengths'), suggestions: explain('improvements'), dataGaps: explain('riskWarnings'),
|
||||||
|
disclaimer: draftText(result, 'disclaimer'), matchingMessage: '产品资料及匹配规则尚未就绪,本报告未执行产品匹配。', modelCode: draftText(result, 'modelCode'),
|
||||||
|
demand, completenessText, completenessItems, readinessLabel: readiness['status'] == 'evaluated' ? draftText(readiness, 'label') : '未评定',
|
||||||
|
readinessBasis: basis.map((item: UTSJSONObject): string => draftText(item, 'message')), warnings: result['systemWarnings'] as string[] }
|
||||||
|
}
|
||||||
|
const observations = (result['observations'] as UTSJSONObject[]).map((item: UTSJSONObject): string => {
|
||||||
|
const fact = rawFacts.find((entry: UTSJSONObject): boolean => entry['id'] == item['factId'])
|
||||||
|
return (fact == null ? '' : draftText(fact, 'label') + ':') + draftText(item, 'text')
|
||||||
|
})
|
||||||
|
return { version, summary: draftText(result, 'summary'), facts, observations, suggestions: result['suggestions'] as string[], dataGaps: result['dataGaps'] as string[], disclaimer: draftText(result, 'disclaimer'), matchingMessage: draftText(result, 'matchingMessage'), modelCode: draftText(result, 'modelCode'), demand: [], completenessText: '旧版报告未计算资料完整度', completenessItems: [], readinessLabel: '未评定', readinessBasis: [], warnings: [] }
|
||||||
|
}
|
||||||
|
|
||||||
|
function enumValue(value: string, labels: string[], codes: string[]): string | null {
|
||||||
|
if (value.length == 0) return null
|
||||||
|
const index = labels.indexOf(value)
|
||||||
|
if (index < 0) throw new Error('选项已变化,请重新选择')
|
||||||
|
return codes[index]
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createAssessment(payload: UTSJSONObject): Promise<string> {
|
||||||
|
const session = getSession()
|
||||||
|
if (session == null) throw new Error('请先登录')
|
||||||
|
const demandError = demandValidationMessage(draftText(payload, 'requestedAmountWan'), draftText(payload, 'requestedTermMonths'), draftText(payload, 'purposeCode'), draftText(payload, 'purposeDetail'))
|
||||||
|
if (demandError.length > 0) throw new Error(demandError)
|
||||||
|
const documents = payload['documents'] as UTSJSONObject[] | null
|
||||||
|
const grants = payload['authorizations'] as string[] | null
|
||||||
|
if ((documents != null && documents.length > 0) || (grants != null && grants.length > 0)) throw new Error('资料上传与外部查询授权尚未开放,请移除资料及授权选择后提交')
|
||||||
|
const values = assessmentApiPayload(payload)
|
||||||
|
const form = new UTSJSONObject()
|
||||||
|
const textKeys = ['name', 'idCard', 'phone', 'city', 'occupation', 'company', 'enterpriseName', 'businessCode']
|
||||||
|
const moneyKeys = ['monthlyIncome', 'annualRevenue', 'totalDebtBalance', 'monthlyLoanPayment', 'creditCardTotalLimit', 'creditCardUsedLimit']
|
||||||
|
textKeys.concat(moneyKeys).forEach((key: string) => { const value = draftText(values, key).trim(); form[key] = value.length == 0 ? null : value })
|
||||||
|
if (form['idCard'] != null) form['idCard'] = (form['idCard'] as string).toUpperCase()
|
||||||
|
if (form['businessCode'] != null) form['businessCode'] = (form['businessCode'] as string).toUpperCase()
|
||||||
|
const countKeys = ['creditLoanCount', 'creditCardCount', 'operatingMonths']
|
||||||
|
countKeys.forEach((key: string) => {
|
||||||
|
const value = draftText(values, key)
|
||||||
|
if (value.length > 0 && !/^[0-9]{1,4}$/.test(value)) throw new Error('数量与经营月数须为0至9999的整数')
|
||||||
|
form[key] = value.length == 0 ? null : parseInt(value)
|
||||||
|
})
|
||||||
|
form['maritalStatus'] = enumValue(draftText(values, 'maritalStatus'), ['已婚', '未婚', '离异', '丧偶'], ['married', 'single', 'divorced', 'widowed'])
|
||||||
|
const socialKeys = ['socialSecurity', 'housingFund']
|
||||||
|
socialKeys.forEach((key: string) => { form[key] = enumValue(draftText(values, key), ['连续缴纳 6 个月以上', '缴纳不足 6 个月', '未缴纳'], ['six_months_plus', 'under_six_months', 'none']) })
|
||||||
|
const ownershipKeys = ['house', 'car']
|
||||||
|
ownershipKeys.forEach((key: string) => { form[key] = enumValue(draftText(values, key), ['有', '无'], ['yes', 'no']) })
|
||||||
|
const cityNames = ['深圳市', '广州市', '北京市', '上海市', '杭州市']
|
||||||
|
const cityCodes = ['440300', '440100', '110100', '310100', '330100']
|
||||||
|
const cityIndex = cityNames.indexOf(draftText(values, 'city'))
|
||||||
|
form['cityCode'] = cityIndex < 0 ? null : cityCodes[cityIndex]
|
||||||
|
// 需求金额单独命名单位,仅在提交边界换算,避免与收入及负债混用。
|
||||||
|
form['requestedAmountYuan'] = wanToYuan(draftText(payload, 'requestedAmountWan'))
|
||||||
|
form['requestedTermMonths'] = parseInt(draftText(payload, 'requestedTermMonths'))
|
||||||
|
form['purposeCode'] = draftText(payload, 'purposeCode')
|
||||||
|
form['purposeDetail'] = form['purposeCode'] == 'other' ? draftText(payload, 'purposeDetail').trim() : null
|
||||||
|
const body: UTSJSONObject = { inputVersion: 2, type: values['type'], moneyUnit: 'yuan', form, documents: [] as UTSJSONObject[], consentIds: [] as string[] }
|
||||||
|
body['requestId'] = assessmentRequestKey(session['token'] as string, JSON.stringify(body))
|
||||||
|
const result = await requestJson('/assessments', 'POST', body)
|
||||||
|
const id = draftText(result, 'assessmentId')
|
||||||
|
if (id.length == 0) throw new Error('提交回执不完整,请重试')
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchAssessments(cursor: string): Promise<AssessmentPage> {
|
||||||
|
const result = await requestJson('/assessments', 'GET', { limit: '20', cursor })
|
||||||
|
const items = result['items'] as UTSJSONObject[] | null
|
||||||
|
return { items: items == null ? [] : items.map((item: UTSJSONObject): AssessmentRecord => recordFrom(item)), hasMore: result['hasMore'] == true, nextCursor: draftText(result, 'nextCursor') }
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchAssessmentStatus(id: string): Promise<AssessmentRecord> {
|
||||||
|
return recordFrom(await requestJson('/assessments/' + encodeURIComponent(id) + '/status', 'GET', {}))
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchAssessmentDetail(id: string): Promise<AssessmentDetail> {
|
||||||
|
const result = await requestJson('/assessments/' + encodeURIComponent(id), 'GET', {})
|
||||||
|
const form = result['form'] as UTSJSONObject | null
|
||||||
|
if (form == null) throw new Error('评估详情不完整')
|
||||||
|
const fields: AssessmentField[] = []
|
||||||
|
const keys = ['name', 'phone', 'city', 'idCard', 'enterpriseName', 'businessCode', 'occupation', 'company', 'operatingMonths', 'monthlyIncome', 'annualRevenue', 'creditLoanCount', 'totalDebtBalance', 'monthlyLoanPayment', 'creditCardCount', 'creditCardTotalLimit', 'creditCardUsedLimit', 'maritalStatus', 'socialSecurity', 'housingFund', 'house', 'car']
|
||||||
|
const legacy = result['inputVersion'] == null || result['inputVersion'] == 1
|
||||||
|
// 历史记录使用提交时冻结的用途标签,不拿当前字典重新解释旧需求。
|
||||||
|
fields.push({ label: '融资金额', value: legacy ? '当时未采集' : yuanToWan(draftText(form, 'requestedAmountYuan')) + ' 万元' } as AssessmentField)
|
||||||
|
const term = form['requestedTermMonths'] as number | null
|
||||||
|
fields.push({ label: '融资期限', value: legacy || term == null ? '当时未采集' : term.toString() + ' 个月' } as AssessmentField)
|
||||||
|
fields.push({ label: '资金用途', value: legacy ? '当时未采集' : draftText(form, 'purposeLabel') } as AssessmentField)
|
||||||
|
if (form['purposeCode'] == 'other') fields.push({ label: '用途说明', value: draftText(form, 'purposeDetail') } as AssessmentField)
|
||||||
|
const labels = ['姓名 / 联系人', '手机号', '城市', '身份证号', '企业名称', '信用代码', '职业 / 行业', '工作单位', '经营月数', '月收入', '年营收', '信用贷款笔数', '欠款总余额', '贷款月还款金额', '信用卡张数', '信用卡总额度', '信用卡已用额度', '婚姻状况', '社保缴纳', '公积金缴纳', '是否有房', '是否有车']
|
||||||
|
const moneyKeys = ['monthlyIncome', 'annualRevenue', 'totalDebtBalance', 'monthlyLoanPayment', 'creditCardTotalLimit', 'creditCardUsedLimit']
|
||||||
|
const enumLabels: UTSJSONObject = { married: '已婚', single: '未婚', divorced: '离异', widowed: '丧偶', yes: '有', no: '无', six_months_plus: '连续缴纳6个月以上', under_six_months: '缴纳不足6个月', none: '未缴纳' }
|
||||||
|
keys.forEach((key: string, index: number) => {
|
||||||
|
const raw = form[key]
|
||||||
|
let value = raw == null ? '未填写' : (typeof raw == 'number' ? (raw as number).toString() : raw as string)
|
||||||
|
if (raw != null && moneyKeys.includes(key)) value = yuanToWan(value) + ' 万元'
|
||||||
|
if (raw != null && key == 'operatingMonths') value += ' 个月'
|
||||||
|
if (['maritalStatus', 'socialSecurity', 'housingFund', 'house', 'car'].includes(key) && enumLabels[value] != null) value = enumLabels[value] as string
|
||||||
|
const personalOnly = ['idCard', 'company', 'monthlyIncome', 'maritalStatus', 'socialSecurity', 'housingFund']
|
||||||
|
const enterpriseOnly = ['enterpriseName', 'businessCode', 'operatingMonths', 'annualRevenue']
|
||||||
|
if ((result['type'] == 'enterprise' && personalOnly.includes(key)) || (result['type'] == 'personal' && enterpriseOnly.includes(key))) return
|
||||||
|
const field: AssessmentField = { label: labels[index], value }; fields.push(field)
|
||||||
|
})
|
||||||
|
return { record: recordFrom(result), fields }
|
||||||
|
}
|
||||||
+43
-3
@@ -1,5 +1,7 @@
|
|||||||
/** Shared request, configuration and session infrastructure. */
|
/** Shared request, configuration and session infrastructure. */
|
||||||
import { API_BASE_URL, APP_ENVIRONMENT } from './config.uts'
|
import { API_BASE_URL, APP_ENVIRONMENT } from './config.uts'
|
||||||
|
import { clearAccountCache } from './cache-lifecycle.uts'
|
||||||
|
import { clearAssessmentFlow } from './assessment-flow.uts'
|
||||||
export { API_BASE_URL, API_DEMO_ENABLED } from './config.uts'
|
export { API_BASE_URL, API_DEMO_ENABLED } from './config.uts'
|
||||||
const REQUEST_TIMEOUT = 10000
|
const REQUEST_TIMEOUT = 10000
|
||||||
const SESSION_KEY = 'aiFinanceSession'
|
const SESSION_KEY = 'aiFinanceSession'
|
||||||
@@ -10,6 +12,8 @@ export function saveSession(token: string, phone: string, userId: string, expire
|
|||||||
if (token.length == 0 || userId.length == 0 || expiresAt.length == 0) throw new Error('登录响应不完整')
|
if (token.length == 0 || userId.length == 0 || expiresAt.length == 0) throw new Error('登录响应不完整')
|
||||||
const expiry = Date.parse(expiresAt)
|
const expiry = Date.parse(expiresAt)
|
||||||
if (isNaN(expiry) || expiry <= Date.now()) throw new Error('登录凭证已过期')
|
if (isNaN(expiry) || expiry <= Date.now()) throw new Error('登录凭证已过期')
|
||||||
|
const previous = getSession()
|
||||||
|
if (previous != null && previous['userId'] != userId) clearAccountCache(previous['userId'] as string)
|
||||||
uni.setStorageSync(SESSION_KEY, { token, phone, userId, expiresAt, loggedInAt: Date.now() })
|
uni.setStorageSync(SESSION_KEY, { token, phone, userId, expiresAt, loggedInAt: Date.now() })
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -21,16 +25,24 @@ export function getSession(): UTSJSONObject | null {
|
|||||||
const userId = session['userId'] as string | null
|
const userId = session['userId'] as string | null
|
||||||
const expiresAt = session['expiresAt'] as string | null
|
const expiresAt = session['expiresAt'] as string | null
|
||||||
if (token == null || userId == null || expiresAt == null || token.length == 0 || userId.length == 0 || token.startsWith('demo-')) {
|
if (token == null || userId == null || expiresAt == null || token.length == 0 || userId.length == 0 || token.startsWith('demo-')) {
|
||||||
uni.removeStorageSync(SESSION_KEY)
|
clearSession()
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
const expiry = Date.parse(expiresAt)
|
const expiry = Date.parse(expiresAt)
|
||||||
if (isNaN(expiry) || expiry <= Date.now()) { uni.removeStorageSync(SESSION_KEY); return null }
|
if (isNaN(expiry) || expiry <= Date.now()) { clearSession(); return null }
|
||||||
return session
|
return session
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isLoggedIn(): boolean { return getSession() != null }
|
export function isLoggedIn(): boolean { return getSession() != null }
|
||||||
export function clearSession(): void { uni.removeStorageSync(SESSION_KEY) }
|
export function clearSession(): void {
|
||||||
|
clearAssessmentFlow()
|
||||||
|
const value = uni.getStorageSync(SESSION_KEY)
|
||||||
|
if (value != null && typeof value == 'object') {
|
||||||
|
const userId = (value as UTSJSONObject)['userId'] as string | null
|
||||||
|
if (userId != null) clearAccountCache(userId)
|
||||||
|
}
|
||||||
|
uni.removeStorageSync(SESSION_KEY)
|
||||||
|
}
|
||||||
const currentToken = (): string => { const session = getSession(); return session == null ? '' : session['token'] as string }
|
const currentToken = (): string => { const session = getSession(); return session == null ? '' : session['token'] as string }
|
||||||
|
|
||||||
const apiMessage = (error: any | null, fallback: string): string => {
|
const apiMessage = (error: any | null, fallback: string): string => {
|
||||||
@@ -75,3 +87,31 @@ const requestJson = (path: string, method: RequestMethod, payload: UTSJSONObject
|
|||||||
}
|
}
|
||||||
|
|
||||||
export { apiMessage, requestJson, wait }
|
export { apiMessage, requestJson, wait }
|
||||||
|
|
||||||
|
export function uploadJson(path: string, filePath: string): Promise<UTSJSONObject> {
|
||||||
|
return new Promise<UTSJSONObject>((resolve, reject) => {
|
||||||
|
const sentToken = currentToken()
|
||||||
|
if (sentToken.length == 0) { reject(new Error('请先登录')); return }
|
||||||
|
if (API_BASE_URL.length == 0 || (APP_ENVIRONMENT == 'production' && !API_BASE_URL.startsWith('https://'))) {
|
||||||
|
reject(new Error('服务地址尚未配置')); return
|
||||||
|
}
|
||||||
|
uni.uploadFile({ url: API_BASE_URL + path, filePath, name: 'file', timeout: REQUEST_TIMEOUT,
|
||||||
|
header: { Authorization: 'Bearer ' + sentToken, 'X-Requested-With': 'XMLHttpRequest' },
|
||||||
|
success: (response) => {
|
||||||
|
if (currentToken() != sentToken) { reject(new Error('登录状态已变化,请重新操作')); return }
|
||||||
|
if (response.statusCode == 401) clearSession()
|
||||||
|
try {
|
||||||
|
const body = JSON.parse<UTSJSONObject>(response.data)
|
||||||
|
if (body == null) throw new Error('上传响应格式错误')
|
||||||
|
if (response.statusCode < 200 || response.statusCode >= 300 || body['code'] != 1) {
|
||||||
|
if (body['code'] == 401) clearSession()
|
||||||
|
throw new Error((body['msg'] as string | null) ?? '头像上传失败')
|
||||||
|
}
|
||||||
|
const data = body['data'] as UTSJSONObject | null
|
||||||
|
if (data == null) throw new Error('上传响应不完整')
|
||||||
|
resolve(data)
|
||||||
|
} catch (error) { reject(error) }
|
||||||
|
}, fail: () => { reject(new Error('头像上传失败,请检查网络后重试')) }
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { requestJson } from './api.uts'
|
||||||
|
import { wanAmountValid, wanToCents } from './money.uts'
|
||||||
|
|
||||||
|
export type FinancingPurpose = { type: string, code: string, label: string, requiresDetail: boolean }
|
||||||
|
|
||||||
|
// 用途由服务端发布;加载失败时提示重试,不用本机猜测字典后提交。
|
||||||
|
export async function fetchFinancingPurposes(): Promise<FinancingPurpose[]> {
|
||||||
|
const result = await requestJson('/dictionaries', 'GET', {})
|
||||||
|
const versions = result['assessmentInputVersions'] as number[] | null
|
||||||
|
const minimum = result['minNewAssessmentInputVersion'] as number | null
|
||||||
|
if (versions == null || !versions.includes(2) || minimum == null || minimum > 2) throw new Error('评估版本已更新,请刷新页面后重试')
|
||||||
|
const dictionary = result['financingPurposes'] as UTSJSONObject | null
|
||||||
|
if (dictionary == null) throw new Error('资金用途暂不可用,请重试')
|
||||||
|
const items = dictionary['items'] as UTSJSONObject[] | null
|
||||||
|
if (items == null) throw new Error('资金用途暂不可用,请重试')
|
||||||
|
const purposes: FinancingPurpose[] = []
|
||||||
|
items.forEach((item: UTSJSONObject) => {
|
||||||
|
if (item['enabled'] == true) purposes.push({ type: item['type'] as string, code: item['code'] as string, label: item['label'] as string, requiresDetail: item['requiresDetail'] == true } as FinancingPurpose)
|
||||||
|
})
|
||||||
|
if (purposes.length == 0) throw new Error('资金用途暂不可用,请重试')
|
||||||
|
return purposes
|
||||||
|
}
|
||||||
|
|
||||||
|
export function demandValidationMessage(amount: string, term: string, code: string, detail: string): string {
|
||||||
|
if (!wanAmountValid(amount) || wanToCents(amount) <= 0) return '请填写大于零的融资金额(万元,最多六位小数)'
|
||||||
|
if (!/^[1-9][0-9]{0,3}$/.test(term)) return '融资期限请输入1至9999的整数月'
|
||||||
|
if (code.length == 0) return '请选择资金用途'
|
||||||
|
if (code == 'other' && (detail.trim().length < 2 || detail.trim().length > 200 || /[<>\x00-\x1f\x7f]/.test(detail))) return '其他用途请填写2至200字说明,不含标签或控制字符'
|
||||||
|
if (code != 'other' && detail.length > 0) return '仅其他用途需要填写说明'
|
||||||
|
return ''
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
// 仅供当前填写页与确认页传递数据,不写本机存储、不发起云端保存。
|
||||||
|
let currentToken = ''
|
||||||
|
let currentInput: UTSJSONObject | null = null
|
||||||
|
let submissionFingerprint = ''
|
||||||
|
let submissionKey = ''
|
||||||
|
|
||||||
|
export function clearAssessmentFlow(): void { currentToken = ''; currentInput = null; submissionFingerprint = ''; submissionKey = '' }
|
||||||
|
// 只在本次流程内复用幂等键:网络超时可安全重试,修改提交内容时生成新键。
|
||||||
|
export function assessmentRequestKey(token: string, fingerprint: string): string {
|
||||||
|
const signature = token + ':' + fingerprint
|
||||||
|
if (submissionFingerprint != signature || submissionKey.length == 0) {
|
||||||
|
submissionFingerprint = signature
|
||||||
|
submissionKey = 'assessment_' + Date.now().toString() + '_' + Math.random().toString(36).substring(2, 14)
|
||||||
|
}
|
||||||
|
return submissionKey
|
||||||
|
}
|
||||||
|
export function readAssessmentFlow(token: string): UTSJSONObject | null {
|
||||||
|
if (token.length == 0 || token != currentToken) { clearAssessmentFlow(); return null }
|
||||||
|
return currentInput
|
||||||
|
}
|
||||||
|
export function writeAssessmentFlow(token: string, input: UTSJSONObject): void {
|
||||||
|
if (token.length == 0) { clearAssessmentFlow(); return }
|
||||||
|
currentToken = token; currentInput = input
|
||||||
|
}
|
||||||
+29
-43
@@ -1,5 +1,7 @@
|
|||||||
import { getUserStorage, setUserStorage } from '@/services/storage.uts'
|
import { getSession } from './api.uts'
|
||||||
import { wanAmountValid, yuanToWan, wanToYuan } from './money.uts'
|
import { readAssessmentFlow, writeAssessmentFlow, clearAssessmentFlow } from './assessment-flow.uts'
|
||||||
|
export { clearAssessmentFlow } from './assessment-flow.uts'
|
||||||
|
import { wanAmountValid, wanToYuan, wanToCents } from './money.uts'
|
||||||
|
|
||||||
export type AssessmentForm = {
|
export type AssessmentForm = {
|
||||||
name: string, idCard: string, phone: string, city: string, maritalStatus: string,
|
name: string, idCard: string, phone: string, city: string, maritalStatus: string,
|
||||||
@@ -7,46 +9,30 @@ export type AssessmentForm = {
|
|||||||
housingFund: string, house: string, car: string, savings: string, creditCard: string,
|
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
|
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: '' }
|
return { name: '', idCard: '', phone: '', city: '', maritalStatus: '', occupation: '', company: '', monthlyIncome: '', socialSecurity: '', housingFund: '', house: '', car: '', savings: '', creditCard: '', otherAssets: '', creditLoanCount: '', totalDebtBalance: '', monthlyLoanPayment: '', creditCardCount: '', creditCardTotalLimit: '', creditCardUsedLimit: '', enterpriseName: '', businessCode: '', annualRevenue: '', operatingYears: '', operatingMonths: '', requestedAmountWan: '', requestedTermMonths: '', purposeCode: '', purposeLabel: '', purposeDetail: '' }
|
||||||
}
|
}
|
||||||
export function draftText(saved: UTSJSONObject, key: string): string {
|
export function draftText(saved: UTSJSONObject, key: string): string {
|
||||||
const value = saved[key]
|
const value = saved[key]
|
||||||
return typeof value == 'string' ? value as string : ''
|
return typeof value == 'string' ? value as string : ''
|
||||||
}
|
}
|
||||||
|
function flowToken(): string { const session = getSession(); return session == null ? '' : session['token'] as string }
|
||||||
export function activeAssessmentType(): string {
|
export function activeAssessmentType(): string {
|
||||||
const type = getUserStorage('assessmentActiveType')
|
const current = readAssessmentFlow(flowToken())
|
||||||
if (type == 'enterprise' || type == 'personal') return type as string
|
return current == null ? 'personal' : draftText(current, 'type')
|
||||||
const legacy = getUserStorage('assessmentDraft')
|
|
||||||
if (legacy != null && typeof legacy == 'object' && draftText(legacy as UTSJSONObject, 'type') == 'enterprise') return 'enterprise'
|
|
||||||
return 'personal'
|
|
||||||
}
|
}
|
||||||
export function loadAssessmentDraft(type: string): UTSJSONObject | null {
|
export function getAssessmentInput(type: string): UTSJSONObject | null {
|
||||||
const scoped = getUserStorage('assessmentDraft:' + type)
|
const current = readAssessmentFlow(flowToken())
|
||||||
if (scoped != null && typeof scoped == 'object') return normalizeAssessmentDraft(scoped as UTSJSONObject)
|
return current != null && current['type'] == type ? current : null
|
||||||
const legacy = getUserStorage('assessmentDraft')
|
|
||||||
if (legacy != null && typeof legacy == 'object') {
|
|
||||||
const saved = legacy as UTSJSONObject
|
|
||||||
if (draftText(saved, 'type') == type) return normalizeAssessmentDraft(saved)
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
export function normalizeAssessmentDraft(saved: UTSJSONObject): UTSJSONObject {
|
|
||||||
if (saved['moneyUnit'] == 'wan') return saved
|
|
||||||
const result: UTSJSONObject = { ...saved, moneyUnit: 'wan', draftVersion: 2 }
|
|
||||||
// 老草稿只有年营收以万元保存,其余金额均为元;只转换一次。
|
|
||||||
const keys = ['monthlyIncome', 'totalDebtBalance', 'monthlyLoanPayment', 'creditCardTotalLimit', 'creditCardUsedLimit']
|
|
||||||
if (saved['moneyUnit'] == 'yuan') keys.push('annualRevenue')
|
|
||||||
keys.forEach((key) => { result[key] = yuanToWan(draftText(saved, key)) })
|
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
export function assessmentApiPayload(saved: UTSJSONObject): UTSJSONObject {
|
export function assessmentApiPayload(saved: UTSJSONObject): UTSJSONObject {
|
||||||
const normalized = normalizeAssessmentDraft(saved)
|
const normalized = saved
|
||||||
const result: UTSJSONObject = { ...normalized, moneyUnit: 'yuan' }
|
const result: UTSJSONObject = { ...normalized, moneyUnit: 'yuan' }
|
||||||
const keys = ['annualRevenue', 'monthlyIncome', 'totalDebtBalance', 'monthlyLoanPayment', 'creditCardTotalLimit', 'creditCardUsedLimit']
|
const keys = ['annualRevenue', 'monthlyIncome', 'totalDebtBalance', 'monthlyLoanPayment', 'creditCardTotalLimit', 'creditCardUsedLimit']
|
||||||
keys.forEach((key) => {
|
keys.forEach((key) => {
|
||||||
@@ -55,17 +41,18 @@ export function assessmentApiPayload(saved: UTSJSONObject): UTSJSONObject {
|
|||||||
})
|
})
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
export function formFromDraft(saved: UTSJSONObject): AssessmentForm {
|
export function formFromInput(saved: UTSJSONObject): AssessmentForm {
|
||||||
return {
|
return {
|
||||||
name: draftText(saved, 'name'), idCard: draftText(saved, 'idCard'), phone: draftText(saved, 'phone'), city: draftText(saved, 'city'), maritalStatus: draftText(saved, 'maritalStatus'),
|
name: draftText(saved, 'name'), idCard: draftText(saved, 'idCard'), phone: draftText(saved, 'phone'), city: draftText(saved, 'city'), maritalStatus: draftText(saved, 'maritalStatus'),
|
||||||
occupation: draftText(saved, 'occupation'), company: draftText(saved, 'company'), monthlyIncome: draftText(saved, 'monthlyIncome'), socialSecurity: draftText(saved, 'socialSecurity'), housingFund: draftText(saved, 'housingFund'),
|
occupation: draftText(saved, 'occupation'), company: draftText(saved, 'company'), monthlyIncome: draftText(saved, 'monthlyIncome'), socialSecurity: draftText(saved, 'socialSecurity'), housingFund: draftText(saved, 'housingFund'),
|
||||||
house: ownershipFromDraft(saved, 'house'), car: ownershipFromDraft(saved, 'car'), savings: draftText(saved, 'savings'), creditCard: draftText(saved, 'creditCard'), otherAssets: draftText(saved, 'otherAssets'),
|
house: ownershipFromInput(saved, 'house'), car: ownershipFromInput(saved, 'car'), savings: draftText(saved, 'savings'), creditCard: draftText(saved, 'creditCard'), otherAssets: draftText(saved, 'otherAssets'),
|
||||||
creditLoanCount: draftText(saved, 'creditLoanCount'), totalDebtBalance: draftText(saved, 'totalDebtBalance'), monthlyLoanPayment: draftText(saved, 'monthlyLoanPayment'),
|
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')
|
enterpriseName: draftText(saved, 'enterpriseName'), businessCode: draftText(saved, 'businessCode'), annualRevenue: draftText(saved, 'annualRevenue'), operatingYears: draftText(saved, 'operatingYears'), operatingMonths: draftText(saved, 'operatingMonths'),
|
||||||
|
requestedAmountWan: draftText(saved, 'requestedAmountWan'), requestedTermMonths: draftText(saved, 'requestedTermMonths'), purposeCode: draftText(saved, 'purposeCode'), purposeLabel: draftText(saved, 'purposeLabel'), purposeDetail: draftText(saved, 'purposeDetail')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export function ownershipFromDraft(saved: UTSJSONObject, key: string): string {
|
export function ownershipFromInput(saved: UTSJSONObject, key: string): string {
|
||||||
const value = draftText(saved, key)
|
const value = draftText(saved, key)
|
||||||
if (value == '有' || value == '无') return value
|
if (value == '有' || value == '无') return value
|
||||||
if (value == '无房产' || value == '无车辆') return '无'
|
if (value == '无房产' || value == '无车辆') return '无'
|
||||||
@@ -83,9 +70,10 @@ export function assetValidationMessage(form: AssessmentForm): string {
|
|||||||
for (let i = 0; i < amounts.length; i++) {
|
for (let i = 0; i < amounts.length; i++) {
|
||||||
if (amounts[i].length > 0 && !wanAmountValid(amounts[i])) return amountLabels[i] + '范围为 0–99999.999999 万元,最多六位小数'
|
if (amounts[i].length > 0 && !wanAmountValid(amounts[i])) return amountLabels[i] + '范围为 0–99999.999999 万元,最多六位小数'
|
||||||
}
|
}
|
||||||
|
if (form.creditCardTotalLimit.length > 0 && form.creditCardUsedLimit.length > 0 && wanToCents(form.creditCardUsedLimit) > wanToCents(form.creditCardTotalLimit)) return '信用卡已使用额度不能大于总额度'
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
export function documentsFromDraft(saved: UTSJSONObject): SelectedDocument[] {
|
export function documentsFromInput(saved: UTSJSONObject): SelectedDocument[] {
|
||||||
const results: SelectedDocument[] = []
|
const results: SelectedDocument[] = []
|
||||||
const items = saved['documents'] as UTSJSONObject[] | null
|
const items = saved['documents'] as UTSJSONObject[] | null
|
||||||
if (items == null) return results
|
if (items == null) return results
|
||||||
@@ -95,22 +83,20 @@ export function documentsFromDraft(saved: UTSJSONObject): SelectedDocument[] {
|
|||||||
})
|
})
|
||||||
return results
|
return results
|
||||||
}
|
}
|
||||||
export function saveAssessmentDraft(type: string, step: number, form: AssessmentForm, documents: SelectedDocument[], authorizations: DataAuthorization[], documentConsent: boolean, dataConsent: boolean): void {
|
export function setAssessmentInput(type: string, step: number, form: AssessmentForm, documents: SelectedDocument[], authorizations: DataAuthorization[], documentConsent: boolean, dataConsent: boolean): void {
|
||||||
const files: UTSJSONObject[] = documents.map((item): UTSJSONObject => ({ name: item.name, path: item.path }))
|
const 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', draftVersion: 2, name: form.name, idCard: form.idCard, phone: form.phone, city: form.city, maritalStatus: form.maritalStatus,
|
type, step, moneyUnit: 'wan', name: form.name, idCard: form.idCard, phone: form.phone, city: form.city, maritalStatus: form.maritalStatus,
|
||||||
occupation: form.occupation, company: form.company, monthlyIncome: form.monthlyIncome, socialSecurity: form.socialSecurity, housingFund: form.housingFund,
|
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,
|
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
|
||||||
}
|
}
|
||||||
setUserStorage('assessmentActiveType', type)
|
writeAssessmentFlow(flowToken(), payload)
|
||||||
setUserStorage('assessmentDraft:' + type, payload)
|
|
||||||
// 保留现有提交与档案入口读取的活动草稿。
|
|
||||||
setUserStorage('assessmentDraft', payload)
|
|
||||||
}
|
}
|
||||||
export function showAgreementNotice(title: string): void {
|
export function showAgreementNotice(title: string): void {
|
||||||
uni.showModal({ title, content: '完整协议将在相关服务开放时提供。当前勾选仅保存您的选择,不会发起实名认证、查询外部数据或上传资料。', showCancel: false, confirmText: '我知道了' })
|
uni.showModal({ title, content: '完整协议将在相关服务开放时提供。当前勾选仅保存您的选择,不会发起实名认证、查询外部数据或上传资料。', showCancel: false, confirmText: '我知道了' })
|
||||||
@@ -118,13 +104,13 @@ export function showAgreementNotice(title: string): void {
|
|||||||
|
|
||||||
export type AssessmentProgress = { percent: number, basicDone: boolean, incomeDone: boolean, assetsDone: boolean, authorizationSelected: boolean }
|
export type AssessmentProgress = { percent: number, basicDone: boolean, incomeDone: boolean, assetsDone: boolean, authorizationSelected: boolean }
|
||||||
export function assessmentProgress(type: string): AssessmentProgress {
|
export function assessmentProgress(type: string): AssessmentProgress {
|
||||||
const saved = loadAssessmentDraft(type)
|
const saved = getAssessmentInput(type)
|
||||||
if (saved == null) return { percent: 0, basicDone: false, incomeDone: false, assetsDone: false, authorizationSelected: false }
|
if (saved == null) return { percent: 0, basicDone: false, incomeDone: false, assetsDone: false, authorizationSelected: false }
|
||||||
const basic = type == 'enterprise' ? ['enterpriseName', 'businessCode', 'name', 'phone', 'city'] : ['name', 'idCard', 'phone', 'city', 'maritalStatus']
|
const basic = type == 'enterprise' ? ['enterpriseName', 'businessCode', 'name', 'phone', 'city'] : ['name', 'idCard', 'phone', 'city', 'maritalStatus']
|
||||||
const income = type == 'enterprise' ? ['occupation', 'operatingYears', 'annualRevenue'] : ['occupation', 'company', 'monthlyIncome', 'socialSecurity', 'housingFund']
|
const income = type == 'enterprise' ? ['occupation', 'operatingMonths', 'annualRevenue'] : ['occupation', 'company', 'monthlyIncome', 'socialSecurity', 'housingFund']
|
||||||
const assets = ['house', 'car', 'creditLoanCount', 'totalDebtBalance', 'monthlyLoanPayment', 'creditCardCount', 'creditCardTotalLimit', 'creditCardUsedLimit']
|
const assets = ['house', 'car', 'creditLoanCount', 'totalDebtBalance', 'monthlyLoanPayment', 'creditCardCount', 'creditCardTotalLimit', 'creditCardUsedLimit']
|
||||||
const keys = basic.concat(income).concat(assets)
|
const keys = basic.concat(income).concat(assets)
|
||||||
const filled = keys.filter((key) => draftText(saved, key).trim().length > 0).length
|
const filled = keys.filter((key) => draftText(saved, key).trim().length > 0).length
|
||||||
const grants = saved['authorizations'] as string[] | null
|
const grants = saved['authorizations'] as string[] | null
|
||||||
return { percent: Math.round(filled / keys.length * 100), basicDone: basic.every((key) => draftText(saved, key).length > 0), incomeDone: income.every((key) => draftText(saved, key).length > 0), assetsDone: assets.every((key) => draftText(saved, key).trim().length > 0) && assetValidationMessage(formFromDraft(saved)).length == 0, authorizationSelected: documentsFromDraft(saved).length > 0 || (grants != null && grants.length > 0) }
|
return { percent: Math.round(filled / keys.length * 100), basicDone: basic.every((key) => draftText(saved, key).length > 0), incomeDone: income.every((key) => draftText(saved, key).length > 0), assetsDone: assets.every((key) => draftText(saved, key).trim().length > 0) && assetValidationMessage(formFromInput(saved)).length == 0, authorizationSelected: documentsFromInput(saved).length > 0 || (grants != null && grants.length > 0) }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
// 只清理指定账户命名空间,不触及其他账户或旧匿名草稿。
|
||||||
|
export function clearAccountCache(userId: string): void {
|
||||||
|
if (userId.length == 0) return
|
||||||
|
const prefix = 'airz:v1:user:' + userId + ':'
|
||||||
|
uni.getStorageInfoSync().keys.forEach((key: string) => {
|
||||||
|
if (key.startsWith(prefix)) uni.removeStorageSync(key)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 草稿功能已取消,启动时移除旧版本遗留键,不自动导入或恢复敏感表单。
|
||||||
|
export function clearLegacyAssessmentCache(): void {
|
||||||
|
uni.getStorageInfoSync().keys.forEach((key: string) => {
|
||||||
|
if (key == 'assessmentDraft' || key.startsWith('assessmentDraft:') || key.endsWith(':assessmentDraft')
|
||||||
|
|| key.includes(':assessmentDraft:') || key.endsWith(':assessmentActiveType') || key.endsWith(':assessmentEditStep')
|
||||||
|
|| key == 'assessmentActiveType' || key == 'assessmentEditStep') uni.removeStorageSync(key)
|
||||||
|
})
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user