# 产品匹配规则开发契约 版本: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::rule:`、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 `,响应沿用`{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。