Files
airz-uniapp/docs/产品匹配规则开发契约.md
T

299 lines
32 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 产品匹配规则开发契约
版本:1.3。日期:2026-09-24。关联PRODUCT-001、PRODUCT-002、PRODUCT-003。页面消费方式见[04—10方案](原型04-10页面改造与跳转方案.md),完整产品判定/快照/接口语义仍以[数据契约](需求与评估结果数据契约.md)第5节为准。
## 1. 本轮实现边界
后端新增 `application/common/library/ProductMatchRules.php`,引擎版本 `product-hard-rules-v1`,实现规范化硬规则列表校验、精确比较、来源门槛、三态合并和逐规则证据。`tests/product_match_rules.php` 64项内存测试通过;不初始化应用、不访问数据库/Redis/网络,正式worker可保持运行。
新增 `ProductPublication.php`,完成产品独立字段及JSON预检、按个人/企业编译规则,并接入后台Product模型的新增、修改及批量保存路径。73项编译测试、16项事务回滚模型测试和41项基础回归通过,原64项规则回归通过。
新增ProductMatching,已接入评估worker:冻结目录、确定性排序及理由生成,匹配结果与报告同事务落库;详见第7节。**匹配列表/详情读接口和04—10页面尚未完成**,单产品AI解释仍为not_requested。用户已明确允许本地产品库直接上架演示数据,本轮录入6条;名称和来源明确标识演示,不将其视为真实产品验收。
## 2. 上层实施门槛
1. 产品发布时校验独立金额/期限/利率范围、适用对象、地区代码/全国声明、用途依据、来源引用和有效期。regions=[]表示明确全国,null表示未配置;金额或期限为空不是无限制。
2. 由服务端编译器把独立字段及补充eligibility_rules映射为完整硬规则集合,必须包含本次需求金额、期限、用途、对象及地区适用性。没有合法发布预检的产品不得调用本模块后直接视为候选。
3. 省/市/区覆盖需使用可信地区树,不能用自由文本或盲目前缀猜测。本轮编译器复用AssessmentRecords::CITIES的5个城市代码,只支持明确全国或精确城市集合;省区代码、未支持城市拒绝发布。未实现全国行政区层级覆盖。
4. 只对已适用硬规则求值;不适用条件由上层确定性编译器处理,并保留not_applicable依据。当前不支持客户端when表达式、软评分、任意脚本或字符串SQL。
5. 事实映射器从本人冻结评估及经校验的外部结果构建facts。requiredSource=verified只能由真实有效外部证据满足,不能接收客户端/AI自报verified。尚无录入/API支撑的年龄、职业自由文本、流水等规则不在本版字段白名单中,发布前拒绝。
6. 首次执行在正式快照中冻结目录、规则全文、引擎版本、排序版本及证据来源;重试不读新产品或当前新规则。新增匹配阶段必须保留已有needs-report-v2快照读取分支,旧成功报告及已冻结无匹配报告不重算。
7. 产品集合不可用继续返回unavailable;合法执行且零候选才返回completed/0。非法产品配置使匹配failed,不能静默跳过后宣称完整结果。
8. 全部产品判定后,仅eligible按productId升序生成rank;score/scorePolicyVersion=null。每条理由由规则结果模板生成,AI只能解释已有依据。
## 3. 内部规则结构
以下是编译后的内部规则对象结构;独立字段对应的规则由服务端生成,不能原样填进补充条件覆盖产品额度或期限。`eligibility_rules.conditions`中的每条记录须加适用对象包装,完整录入格式见第6节。草稿仍兼容旧JSON;本轮不迁移/改写已有JSON,正式上架采用新增整体预检。
```json
[
{"id":"amount_range","field":"requestedAmountYuan","operator":"between","value":["10000.00","500000.00"],"requiredSource":"self_reported"},
{"id":"term_range","field":"requestedTermMonths","operator":"between","value":[6,36],"requiredSource":"self_reported"},
{"id":"purpose_allowed","field":"purposeCode","operator":"in","value":["education","daily_consumption"],"requiredSource":"self_reported"}
]
```
示例仅用于开发测试,不代表任何真实银行条件,也不是可直接发布的完整产品。validate/evaluate要求非空列表、最多50条;id在产品内唯一、以小写字母开头、最多48字符,仅字母数字下划线短横线。严格接受id/field/operator/value/requiredSource五个键,多键/少键均拒绝;规则版本由上层冻结并传入。
| 字段 | 内部类型/单位 | 支持操作符 |
| --- | --- | --- |
| requestedAmountYuan、monthlyIncome、annualRevenue、totalDebtBalance、monthlyLoanPayment、creditCardTotalLimit、creditCardUsedLimit | money/yuan;0.00—999999999.99两位小数字符串 | eq、in、gte、lte、between |
| requestedTermMonths、operatingMonths | integer/month;非负PHP整数 | 同上 |
| creditLoanCount、creditCardCount | integer/count;非负PHP整数 | 同上 |
| cityCode | region;六位代码字符串,实际行政区有效性由上层字典校验 | eq、in |
| applicantType | enum:personal/enterprise | eq、in |
| purposeCode | enum:首版用途字典中的稳定code | eq、in;actual=other始终unknown |
| socialSecurity、housingFund | enum:none/under_six_months/six_months_plus | eq、in |
| house、car | enum:yes/no | eq、in |
金额不接受float、科学计数法、三位小数、非规范前导零。整数不接受字符串、float、bool;运行模块支持0—4294967295,上层仍须执行具体字段业务上限。in要求1—50个不重复同类型值;between必须两个有序值,含上下边界。枚举不得使用范围运算。新增字段/枚举/操作符必须有版本化兼容和测试,不能运行时接受任意配置。
## 4. 事实、输出与错误
调用为 `ProductMatchRules::evaluate($rules, $facts, $productId, $ruleVersion)`。productId为正整数字符串;ruleVersion为1—64位字母数字点横线下划线字符串。facts结构:
```json
{
"requestedAmountYuan":{"value":"123456.78","source":"self_reported","observedAt":"2026-09-24T00:00:00Z"},
"requestedTermMonths":{"value":12,"source":"self_reported","observedAt":"2026-09-24T00:00:00Z"},
"purposeCode":{"value":"education","source":"self_reported","observedAt":"2026-09-24T00:00:00Z"}
}
```
结果为 `{eligibility,evidence}`,仅表示传入已适用条件集合的结果,未生成rank/score/推荐文案。先校验全部规则再求值,不在首个fail提前返回:有fail则ineligible,无fail有unknown则unknown,否则eligible。
每条证据包含evidenceId=`product:<productId>:rule:<ruleId>`、ruleId、ruleVersion、kind=hard、fieldRefs、operator、expected、actual、outcome、reasonCode、source、requiredSource、observedAt、valueType及unit。金额保持元字符串。source/observedAt在未知时为null,不冒充已核验来源或捏造观测时间;未被引用的额外身份字段不进入输出。该结构当前是内部camelCase对象,后续入库仍按已有契约显式映射snake_case,不直接绕过持久化校验。
| 情况 | outcome / reasonCode | 行为 |
| --- | --- | --- |
| 值缺失/null | unknown / FACT_MISSING | actual=null;不当0 |
| 值类型/枚举/精度非法 | unknown / FACT_INVALID | actual=null;原值不复制到证据或错误日志 |
| 来源未知或不满足verified要求 | unknown / SOURCE_INSUFFICIENT | 保留有效规范值及真实来源,不当已核验 |
| purposeCode=other | unknown / PURPOSE_REQUIRES_REVIEW | 不读取用途自由说明,不自动猜测 |
| 满足/不满足边界 | pass/fail / CONDITION_MET/CONDITION_NOT_MET | 保留预期值、实际值、类型、单位及规则引用 |
| 规则列表/字段/操作符/阈值/标识不合法 | 抛InvalidArgumentException,固定MATCH_RULES_INVALID | 上层转换为配置错误,不作为某用户条件不符 |
## 5. 当前验收与下一交付
64项覆盖金额上下界/一分钱误差/最大值/float拒绝、期限边界/类型、零值与缺失、fail优先但保留unknown、来源门槛、other用途、地区不从文本推断、证据产品隔离、确定性、无评分、身份字段排除、非法规则与列表大小等。
发布预检、字段编译、目录冻结/排序/理由及正式任务持久化已交付。匹配分页/详情与公共详情、03/04/05/06基础现已接通(第8节);下一步为07对比及09报告三栏;PRODUCT-003结构化编辑体验和真实产品录入仍待完成。演示产品联调不解除真实产品资料、外部核验和业务金标准验收的依赖。
## 6. 产品发布JSON与字段编译(本轮已实现)
### 6.1 完整格式
复用`fa_product.eligibility_rules`,不新增表或列。顶层严格要求version/source/purposes/conditions四个键,不接受旧rules或未知键。version为运营维护的正整数规则修订号;编译格式由`product-publication-v1`标识,产品行version独立由服务端递增。
下例只展示结构,来源是合成引用,**不能作为真实产品发布资料**。生产录入时必须替换为可核对的机构材料;程序只校验格式与完整性,不证明材料真实性。
```json
{
"version": 1,
"source": {
"publisher": "合成机构(仅限示例)",
"title": "额度、期限、利率、地区、用途及全部准入条件说明",
"reference": "fixture:test-only",
"checkedOn": "2026-09-24"
},
"purposes": {
"personal": ["education", "daily_consumption"],
"enterprise": ["working_capital"]
},
"conditions": [
{
"applicantTypes": ["personal"],
"rule": {
"id": "income_min",
"field": "monthlyIncome",
"operator": "gte",
"value": "8000.00",
"requiredSource": "self_reported"
}
}
]
}
```
此例对应object="1,2";个人产品只保留personal,企业产品只保留enterprise。purposes的键须恰好覆盖适用对象,每类至少一个来自当前需求字典的用途代码;other仍需人工核对,不会因此自动通过。conditions可以为[],不能为{};即使为空仍生成至少4条必要规则。最多44个补充条件,加上独立字段规则后每分支最多50条。
source为整份产品条件的共同来源:publisher为发布机构(最长100字)、title为资料标题(200字)、reference为可定位的公开文档链接或内部材料索引(500字)、checkedOn为实际核对日期且不得晚于发布日。引用必须覆盖独立字段、用途及全部补充条件,多份资料可由受控材料清单统一索引。不能存用户资料或私密链接令牌;后台不访问该地址,不把此声明当成用户事实的verified来源。
### 6.2 独立字段与规则
| 产品字段 | 发布要求 | 编译结果 |
| --- | --- | --- |
| product_name/product_type/bank_name | 必填,长度100/20/50以内 | 保留规范化公共字段 |
| object | 1个人、2企业或两者;不重复 | 每类独立规则分支,base_object严格eq该分支类型,错误选择分支不能通过 |
| amount_min/amount_max | 元的十进制字符串,0—999999999.99;有序且max大于0 | base_amount对requestedAmountYuan执行between,含边界,不拆额 |
| term_min_months/term_max_months | 1—9999整数月,有序 | base_term对requestedTermMonths执行between,不改期 |
| annual_rate_min/annual_rate_max | DECIMAL(7,4)年化百分比,有序且非负 | 四位小数字符串保留;不把利率本身当申请人准入条件 |
| valid_from/valid_until | 合法日期YYYY-MM-DD且有序;上架时结束日不得早于当天 | 含首尾日期;未来生效可维护,目录服务必须在执行日筛选,不能提前匹配 |
| regions | JSON数组;[]明确全国,null不合法;非空只能填现有城市代码字符串 | nationwide不要求用户城市;exact_city编译base_region/in,城市未知为unknown |
| operating_months_min | 可空;非空为0—9999整数且产品必须含企业对象 | 仅企业分支生成base_operating/gte;混合产品个人分支记录不适用依据 |
| age_min/age_max | 当前必须为空 | 尚无年龄事实采集,不从身份证或AI推断,含年龄硬条件拒绝发布 |
| eligibility_rules.purposes | 按对象分组、依据明确且代码合法 | base_purpose/in,未知/other不当满足 |
现有可用城市为440300深圳、440100广州、110100北京、310100上海、330100杭州,直接复用评估白名单;不是全量行政区字典。未来扩展省市区必须补可信树、版本和测试。
补充rule沿用第3节五键结构,每条外层必须恰含applicantTypes/rule。monthlyIncome/socialSecurity/housingFund只能用于个人,annualRevenue只能用于企业,其余共有事实可明确用于两类。operatingMonths复用独立列,补充条件不能重复指定;金额、期限、对象、地区、用途也禁止覆盖。自定义id全产品唯一,不得使用base_前缀。grade/condition/recommendation/deprecated等文本不执行规则,运营必须将所有硬条件完整结构化,不能只留在说明里。
### 6.3 模型写入与兼容
Product模型保存事务覆盖行锁、预检和写入;编辑合并锁定的最新记录与实际变更字段后验证,避免部分更新漏掉上架状态或使用旧值校验。新增version固定1,更新以最新version加1,不能从表单指定版本。默认后台add/edit/multi均使用模型save;multi任一条失败整批回滚。SQL/Db直写不触发模型事件,未来导入服务必须走模型或同一预检,不得当作发布接口。
草稿允许未齐资料和旧`{"version":1,"rules":[]}`。旧不完整上架产品再次编辑/上架会被拒绝,可先下架再补齐;下架不要求满足新增完整性门槛。本轮不批量清理或自动下架现有数据。后台表单仍为原JSON文本框,默认旧示例仅能存草稿;正式上架请按本节格式录入,结构化编辑器与帮助展示后续补充。
compile(product,dictionary)不读当前时间,返回来源、字典、规范化范围、有效期、地区模式、两类规则及notApplicableByApplicantType。assertPublishable额外按传入日期检查过期和未来核对日期。全国声明由regionMode/regions/source保留,不生成空in;不适用记录包含ruleId和APPLICANT_TYPE_NOT_APPLICABLE,但尚未转换为正式报告证据。
上层ProductMatching已冻结原产品id/version/参数、编译结果及字典,校验上架状态/执行日有效性,按服务端申请类型选分支、建立事实并排序。不得仅因compile成功直接对外推荐,也不得在历史重试时使用新字典或新产品行。
### 6.4 前序发布预检验证记录
2026-09-24,PHP 7.4.3:`tests/product_publication.php`73项、`tests/product_publication_model.php`16项、`tests/product_match_rules.php`64项、`tests/finance.php`41项,共194项通过。覆盖必填/来源/用途/地区、精度/边界/过期、未知字段、覆盖及重复编号、混合对象/分支错用/不适用依据、来源门槛、50条上限、局部更新/旧模型/批量回滚/版本/旧品下架。
新模型测试及基础回归在事务内生成合成数据后回滚,测试Redis会话清理;产品前后全量记录比较一致。没有真实产品发布、数据库迁移、外部模型或生图调用。未修改模板或前端页面,未将模型路径测试描述为后台浏览器端到端验收。正式worker保持运行;本次脚本没有全库任务恢复扫描或评估任务入队。
## 7. 目录冻结、结果持久化与本地演示产品(最新)
### 7.1 执行版本与冻结边界
新执行引擎`needs-report-v2-match-v1`仍输出resultVersion=2;规则执行器`product-matching-v1`、排序`product-id-ascending-v1`。首次worker抢占后、模型调用前,在评估事务内从产品表单次读取switch=1记录,编译并按Asia/Shanghai业务日期(含有效期首尾)筛选有效产品和申请主体,数字产品ID升序冻结。草稿/下架不读;已上架配置损坏使任务失败,不跳过后宣称完整匹配。已上架产品先完整校验再筛有效期,过期旧品有配置错误时也应下架或修复。
执行快照的productCatalog包含目录版本、排序版本、申请主体、evaluatedAt/businessDate、rulesPublished、每项产品ID/版本/参数白名单/编译结果、完整冻结字典和catalogSnapshotId。ID为规范化内容SHA-256;执行快照沿用executionHash/contentHash校验。最多100条上架记录、目录正文最多2MiB,SQL取101条探测超限;超限MATCH_CATALOG_LIMIT,绝不截断。仅纳入某主体的产品形成该评估目录,非该主体不生成“不符合”记录。
旧引擎needs-report-v2已冻结的unavailable报告和失败任务保留旧语义;新任务的目录为空、开关关闭或已有目录均冻结,重试不因产品更新/下架/过期/开关变化改读新目录。产品修订只影响新评估。配置`matching.enabled`默认开启;关闭仅影响未冻结的新评估,reasonCode=MATCH_RULES_UNPUBLISHED。
### 7.2 事实、判定与存储
事实只从服务端只读评估行按白名单映射,金额保持元字符串、整数严格转换、observedAt来自submitted_at。姓名/手机号/身份证/企业名/职业自由文本及客户端source不进入匹配事实。外部核验尚未接入,所以本轮事实来源全部self_reported;产品要求verified时返回unknown,不捏造核验结论。城市有效性使用冻结字典。
全部适用条件执行后fail优先于unknown。按产品ID升序,仅eligible生成连续rank;其他rank=null,全部score/scorePolicyVersion=null。not_applicable记录规则ID、产品版本依据和product_snapshot来源;不因不适用条件要求补填。正向、阻断和待核实理由分别从每条证据模板生成,完整保留、不凑数量;产品级evidenceRefs禁止互相借用。单产品AI解释当前not_requested,DeepSeek只整理自填报告事实,不作匹配决策。
结果由ProductMatching::storage显式映射嵌套键名,match_result保持有序snake_case数组,冻结产品参数、来源、材料/说明、规则和采集/更新时间。Assessment::complete对v2复算冻结结果并核对完整数组及matching统计,支持合法null rank/score;报告、结果、任务成功及日志在同一事务完成,禁止部分成功。v1写入格式保留。
无适用目录:unavailable、计数null;规则完成但无人符合:completed、candidateCount=0,其他两类仍按实际计数。配置错误MATCH_CONFIGURATION_INVALID、快照损坏MATCH_SNAPSHOT_INVALID通过失败状态反馈,不归咎用户。正式报告提供matchingMessage及containsDemoProducts,前端报告适配器显示实际统计;旧报告没有新文案时继续原提示。
### 7.3 本地录入的6条产品
用户明确授权在当前无用户使用的项目内直接录入并上架,替代此前“仅测试目录”的约束。来源文件为后端`database/demo_products.json`;`database/seed_demo_products.php`默认预检,`--apply`写入本机MySQL。脚本限制localhost连接、事务写入并用来源引用识别已存在记录;再次执行只保留,不覆盖运营修改。
| 本地ID | 名称(均带【演示】) | 对象 | 额度(万元) | 期限(月) | 核心演示条件 |
| --- | --- | --- | --- | --- | --- |
| 22 | 个人灵活消费 | 个人 | 1—30 | 3—36 | 全国;月收入≥0.3万元 |
| 23 | 个人小额周转 | 个人 | 1—8 | 3—24 | 深圳/广州;月收入≥0.5万元 |
| 24 | 个人收入核验 | 个人 | 2—50 | 6—60 | 全国;月收入≥0.8万元且需核验依据 |
| 25 | 企业初创周转 | 企业 | 5—50 | 3—36 | 全国;年营收≥10万元;经营0个月可参与判断 |
| 26 | 企业成长经营 | 企业 | 20—300 | 12—60 | 深圳/广州/杭州;年营收≥100万元、经营≥24个月 |
| 27 | 个人企业基础融资 | 两者 | 1—100 | 3—60 | 全国;主体分支用途分别编译 |
以上全部为合成参数,年化范围3.6%—12%,有效期2026-09-24至2030-12-31;数据库存元,表格显示万元。来源reference以`demo:airz-local-products-v1:`开头,报告/快照标演示。不存在真实银行对应关系、渠道授信或实际申请承诺。演示产品可在后台下架;已有报告保留其历史快照。投用前须替换真实资料并下架演示产品,不能只改名称去掉标识。
### 7.4 验证入口
`tests/product_matching.php`纯内存验证目录筛选、冻结摘要、排序、理由引用、来源门槛和配置失败;`tests/product_matching_tasks.php --worker-stopped`使用本地演示目录及随机合成账户/假模型验证正式提交、落库、重试、旧引擎、损坏快照和归属。后者预期6条初始演示参数;运营修改后需要同步业务金标准,不能自动覆盖产品凑测试。测试结束只清理本次账户/评估/会话/队列,6条演示产品按用户要求保留。
本阶段之后的匹配只读API及页面进展见第8节;单产品AI解释、公共产品列表、07—10扩展和真实产品验收仍待完成。
## 8. 匹配读取与页面消费(2026-09-24已实现)
### 8.1 接口、鉴权与分页
统一前缀`/api/v1`,全部要求`Authorization: Bearer <token>`,响应沿用`{code,msg,time,data}`并禁止缓存。以下GET只读,不创建评估、调用DeepSeek或重新计算匹配;重复读取只可能更新当前可用性。
| 方法和路径 | 参数/类型 | 响应data | 关键错误 |
| --- | --- | --- | --- |
| GET /assessments/:id/matches | id正整数字符串;eligibility为eligible/ineligible/unknown,默认eligible;limit查询字符串1—50,默认3;cursor可省略/空字符串 | assessmentId:string、resultVersion:number、matching、items:MatchResult[]、hasMore:boolean、nextCursor:string/null | 401未登录;404非本人/软删除/ID无效;409报告未完成/格式不可用/匹配损坏;422参数/游标错误 |
| GET /assessments/:id/matches/:productId | 两ID均为正整数字符串,无请求体 | assessmentId、resultVersion、matching、item:MatchResult | 404 MATCH_NOT_FOUND表示不在本次快照;其他鉴权/状态错误同列表;不回退公共详情 |
| GET /products/:id | 当前产品ID;无个人评估参数 | 当前公共Product DTO及rulesByApplicantType | 401未登录;404 PRODUCT_NOT_FOUND表示未发布/不在有效期/不存在;409 PRODUCT_CONFIGURATION_INVALID表示发布资料损坏 |
列表拒绝未知查询字段、数组参数和非法limit。先验证所有者,再解析游标,不泄露他人评估是否有匹配结果。游标使用既有AES-GCM加密方案,绑定userId、assessmentId、resultVersion、冻结结果摘要、eligibility及offset;不是页码,不由前端拼接,换组必须丢弃。结果更改、跨评估、跨组或篡改返回422 `MATCH_CURSOR_INVALID`,客户端从首屏重读。当前产品状态不进入摘要,产品下架不会令分页失效。
已匹配但无符合项时`status=completed,candidateCount=0`;未执行时`status=unavailable`、三组统计为null、items空数组,均返回200。v1成功报告返回`LEGACY_INPUT`;既有v2未匹配报告不补算。损坏的匹配列表、重复产品、非法rank/score、统计不一致或错误证据引用返回409 `MATCH_RESULT_UNAVAILABLE`,不以空数组掩盖损坏。
未执行匹配的data示例(ID仅为示意):
```json
{
"assessmentId": "100",
"resultVersion": 1,
"matching": {
"status": "unavailable", "reasonCode": "LEGACY_INPUT",
"ruleVersion": null, "orderingPolicyVersion": null, "catalogSnapshotId": null,
"candidateCount": null, "ineligibleCount": null, "unknownCount": null
},
"items": [], "hasMore": false, "nextCursor": null
}
```
### 8.2 字段投影、金额与历史
MatchResult沿用数据契约第5节:productId、productVersion、eligibility、rank、score、scorePolicyVersion、reasons、positiveReasons、blockingReasons、cautions、evidence、aiExplanation、productSnapshot,新增实时读取的currentAvailability(available/unavailable/unknown)。score无已发布评分策略时为null;rank仅eligible有值,不能渲染成百分比或审批概率。所有理由/evidenceRefs只关联本项证据;接口显式投影字段,不返回analysis_context、姓名、证件号码或未声明内部字段。
productSnapshot包含产品ID/版本/名称/机构/类型,amountMinYuan、amountMaxYuan(元的两位小数字符串)、termMinMonths/termMaxMonths(整数月)、annualRateMinPercent/annualRateMaxPercent(百分数字符串)、isDemo、applicantTypes、regions(空数组表示全国)、validFrom/validUntil(日期)、capturedAt、source(publisher/title/reference/checkedOn)、rules、information/condition及创建/更新时间。客户端仅在显示时转万元,例如`"123456.78"`元显示`12.345678 万元`;个人事实金额也按此规则。
公共DTO使用当前发布条件rulesByApplicantType按主体分组,不返回eligibility、rank、score、个人理由/事实/证据。repaymentMethod、channel、approvalDuration在两种产品DTO均为null,当前页面显示“未提供”;不得从演示或自由文本猜值。公共详情不等于公共产品列表,GET /products尚未实现。
当前可用性通过一次受限ID集合查询读取,避免逐项查询。下架、删除、未到生效日或过期为unavailable;目录查询异常为unknown;历史快照仍可读。不会把产品当前版本/额度拼回历史详情。AI解释目前not_requested,前端明确提示尚未生成,保留完整确定性规则理由;失败时的提示分支已预留,真实生成与计费/重试待后续任务。
### 8.3 页面与验收边界
报告入口携带assessmentId;列表三组统计与分页来自同次结果;卡片只预览最多三条主要理由,详情保留全部不符/待核实理由,已满足项可展开。无评估编号时选择历史或重新评估;非法编号明确错误,绝不读取最近结果。详情无assessmentId只展示公共资料,有assessmentId始终读取本人快照。页面离开清空结果,响应返回时校验当前Token和请求序号,防止旧账户迟到响应恢复界面。
本轮`tests/match_records.php --worker-stopped`62项验证接口身份/方法、三组、游标分页和篡改、快照归属、未知键投影、损坏数据、下架/过期/删除/改价后历史、旧版报告、重复读取无副作用。产品变化只在测试连接事务内发生并回滚,随机账户及队列清理;停止正式worker以避免恢复扫描拾取合成评估。浏览器结果及截图见开发进度6.13。
## 9. 单产品解释任务与原型04/05(2026-09-28已实现,覆盖第7/8节的未接入状态)
### 9.1 接口与状态
`POST /api/v1/assessments/:id/matches/:productId/explanation`要求会员Bearer Token、本人成功且未删除评估及该产品冻结结果,Content-Type为application/json。请求只允许`{"requestId":"explanation_20260928_001"}`;requestId为8—64位ASCII字母/数字/下划线/短横线,无金额或模型参数。重复读取GET不生成解释。
HTTP200沿用`{code:1,msg,time,data}`,data与匹配详情相同,不表示模型已经完成;客户端检查item.aiExplanation.status。queued/running期间轮询原GET匹配详情。例:
```json
{
"status": "completed",
"text": "该项信息来自自填资料,仍需核对证明材料。",
"evidenceRefs": ["product:22:rule:base_amount"],
"reasonExplanations": [{
"reasonId": "product:22:rule:base_amount:reason",
"text": "该项信息来自自填资料,仍需核对证明材料。",
"evidenceRefs": ["product:22:rule:base_amount"]
}],
"generatedAt": "2026-09-28T07:07:00+00:00",
"errorCode": null,
"canRetry": false
}
```
上例只说明结构,不是完整产品样例。text/evidenceRefs兼容已有消费者;逐条渲染使用reasonExplanations,数值/来源仍取确定性证据,不能从模型文本反解析金额。首次not_requested;queued/running保留空文本/数组;失败为failed,errorCode仅固定错误码,canRetry表示未超过三次上限;成功为completed并带生成时间。有效的旧not_requested对象缺新增字段时,前端按空值兼容。
| HTTP/错误 | 含义与行为 |
| --- | --- |
| 401 AUTH_REQUIRED | 会话失效;客户端清理并回登录 |
| 404 ASSESSMENT_NOT_FOUND / MATCH_NOT_FOUND | 非本人、已删记录或产品不在本次快照;不回退公共产品 |
| 409 REPORT_NOT_READY / MATCH_RESULT_UNAVAILABLE | 报告未成功或存储结构损坏,不调用模型 |
| 422 VALIDATION_ERROR | 非法requestId或多余请求字段 |
| 503 ANALYSIS_NOT_CONFIGURED | 模型服务未配置,不预留新调用 |
| 429 AI_DAILY_LIMIT | 与报告共用当日用户预留额度,开发默认二十次 |
| 409 AI_ATTEMPT_LIMIT | 每评估每产品最多三次尝试,不再创建任务 |
模型/网络失败写入状态,不使用供应商异常正文: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等。用户可读状态与规则理由仍保留;不自动重新调用收费模型。
### 9.2 持久化、幂等和恢复
- fa_ai_chat.match_result中的单项ai_explanation保存状态、reason_explanations及生成时间;内部还保存task_id/request_keys/attempts/lease_until。只更新这一个对象,不修改冻结参数、证据、判定、排序、报告或评估生命周期。不同产品更新同一JSON时使用会话行锁,防止相互覆盖。
- fa_ai_chat_log使用role=assistant、request_id前缀`pex:`、pending/streaming/completed/failed以及已存在的模型/Token/耗时/错误字段。用户行锁串行预算预留;会话锁串行序号和快照写入。数据库事务原子保存任务和快照状态,不新增业务表/索引/迁移。
- 相同requestId重放返回当前状态;queued/running期间不同键也合并为同一任务;成功后不同键返回历史解释。明确failed后用户使用新键重试,最多三次。次数包含失败和不确定超时,防止无限收费。
- analysis:work继续优先处理既有报告队列,空闲时领取一条持久解释日志;pending任务无需Redis投递,进程重启不会丢失。租约为max(180秒,供应商超时+60秒),streaming过期改失败;`--recover-only`也回收解释租约,不调用模型。服务停机期间queued保持等待,恢复进程后继续消费。
- 多worker用日志状态和会话锁只允许一次领取。迟到回复不得回写已失效任务,已发生用量仍补记原日志。删除评估/禁用账户后不保存成功解释。运营需关注pending积压、失败错误码和调用用量;当前未实现独立解释后台管理页。
- 分页摘要排除ai_explanation,解释生成不使产品分页失效;其他冻结字段仍参与摘要并校验。部署时已有旧摘要游标可收到一次MATCH_CURSOR_INVALID,重新从首屏读取即可,不改历史报告。
### 9.3 模型边界与页面
使用已配置deepseek-v4-flash-0731,只发该产品规则理由、字段白名单、actual/expected/source/outcome,不发送姓名、电话、证件、附件、机构说明自由文本或完整报告。模型输出仅reasons数组,每项reasonId/text;按ID集合检验恰好覆盖所有理由,无重复、遗漏和外来ID,恢复规则顺序后由服务端附原证据引用。每条最多九十字,数字/金额/期限/HTML/明显授信承诺等拦截。确定性校验不能替代真实产品人工语义金标准,正式机构资料仍待验收。
页面生成期间可离开,重新打开读取保存状态;每三秒查询、最多六十次,断网/轮询结束提供手动查询,不重提任务。页面离开清理个人数据和计时器;迟到回包校验Token/请求序号/可见性。初始依据、AI解释失败、已保存解释可区分;已有数值仍以万元精确展示。布局与有意差异见原型方案第8节。
验证入口:`php tests/product_explanations.php --worker-stopped`(51项,假模型);可选`--live`额外调用一次真实Flash。`php tests/match_records.php --worker-stopped`更新为70项,含解释POST方法、归属、参数、删除记录和幂等回执。必须先停止worker,测试仅清理其随机账号/日志/评估/队列。真实模型和浏览器验证记录见开发进度6.18。