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

197 lines
21 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.2。日期: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用途、地区不从文本推断、证据产品隔离、确定性、无评分、身份字段排除、非法规则与列表大小等。
发布预检、字段编译、目录冻结/排序/理由及正式任务持久化已交付。下一步为匹配分页/详情与公共查询接口,再按04—10方案接页面;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条演示产品按用户要求保留。
正式接口的matches分页/详情、公共产品查询、单产品AI解释及04—10页面仍待后续开发;当前报告页显示真实规则统计,不能据此宣称整个匹配页面已经验收。