hookehuyr

feat(parse): 增强文档解析工具链和智能字段提取

主要改进:
- 优化 smartExtractList() 智能字段提取器
- 增强产品边界检测逻辑
- 完善 MCP 解析切换功能
- 优化 mockData 产品列表数据结构
- 更新计划书模板配置

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
...@@ -5,3 +5,4 @@ ...@@ -5,3 +5,4 @@
5 {"action":"update","backup_file":"/Users/huyirui/program/itomix/git/manulife-weapp/docs/parsed-backup/plan-templates.backup.1771078080604.js","target_file":"/Users/huyirui/program/itomix/git/manulife-weapp/src/config/plan-templates.js","form_sn_list":["savings-readme-a4296d1f"],"at":"2026-02-14T14:08:00.605Z"} 5 {"action":"update","backup_file":"/Users/huyirui/program/itomix/git/manulife-weapp/docs/parsed-backup/plan-templates.backup.1771078080604.js","target_file":"/Users/huyirui/program/itomix/git/manulife-weapp/src/config/plan-templates.js","form_sn_list":["savings-readme-a4296d1f"],"at":"2026-02-14T14:08:00.605Z"}
6 {"action":"update","backup_file":"/Users/huyirui/program/itomix/git/manulife-weapp/docs/parsed-backup/plan-templates.backup.1771078351660.js","target_file":"/Users/huyirui/program/itomix/git/manulife-weapp/src/config/plan-templates.js","form_sn_list":["savings-2-148b3acd"],"at":"2026-02-14T14:12:31.660Z"} 6 {"action":"update","backup_file":"/Users/huyirui/program/itomix/git/manulife-weapp/docs/parsed-backup/plan-templates.backup.1771078351660.js","target_file":"/Users/huyirui/program/itomix/git/manulife-weapp/src/config/plan-templates.js","form_sn_list":["savings-2-148b3acd"],"at":"2026-02-14T14:12:31.660Z"}
7 {"action":"update","backup_file":"/Users/huyirui/program/itomix/git/manulife-weapp/docs/parsed-backup/plan-templates.backup.1771080130974.js","target_file":"/Users/huyirui/program/itomix/git/manulife-weapp/src/config/plan-templates.js","form_sn_list":["savings-2-55bcffc2"],"at":"2026-02-14T14:42:10.974Z"} 7 {"action":"update","backup_file":"/Users/huyirui/program/itomix/git/manulife-weapp/docs/parsed-backup/plan-templates.backup.1771080130974.js","target_file":"/Users/huyirui/program/itomix/git/manulife-weapp/src/config/plan-templates.js","form_sn_list":["savings-2-55bcffc2"],"at":"2026-02-14T14:42:10.974Z"}
8 +{"action":"update","backup_file":"/Users/huyirui/program/itomix/git/manulife-weapp/docs/parsed-backup/plan-templates.backup.1771137003708.js","target_file":"/Users/huyirui/program/itomix/git/manulife-weapp/src/config/plan-templates.js","form_sn_list":["life-insurance-3-d8fde07d"],"at":"2026-02-15T06:30:03.709Z"}
......
This diff is collapsed. Click to expand it.
...@@ -15,6 +15,12 @@ ...@@ -15,6 +15,12 @@
15 * 15 *
16 * # 查看待处理文档 16 * # 查看待处理文档
17 * npm run parse:docs -- --list 17 * npm run parse:docs -- --list
18 + *
19 + * # 应用审核通过的配置
20 + * npm run parse:docs -- --apply=计划书模版4
21 + *
22 + * # 预览应用配置(不实际修改)
23 + * npm run parse:docs -- --apply=计划书模版4 --dry-run
18 */ 24 */
19 import crypto from 'crypto' 25 import crypto from 'crypto'
20 import fs from 'fs' 26 import fs from 'fs'
...@@ -976,17 +982,27 @@ ${code.trim()} ...@@ -976,17 +982,27 @@ ${code.trim()}
976 982
977 ## 📋 审核后操作 983 ## 📋 审核后操作
978 984
979 -### 确认无误 985 +### 方法 1:自动应用(推荐)
986 +\`\`\`bash
987 +# 预览变更(不实际修改)
988 +pnpm parse:docs -- --apply=${baseFileName} --dry-run
989 +
990 +# 确认无误后,正式应用
991 +pnpm parse:docs -- --apply=${baseFileName}
992 +
993 +# 说明:
994 +# 1. 自动提取配置代码并插入到 src/config/plan-templates.js
995 +# 2. 自动创建备份文件(docs/parsed-backup/)
996 +# 3. 自动将审核文件移动到 docs/parse-audit/approved/
997 +\`\`\`
998 +
999 +### 方法 2:手动操作
980 \`\`\`bash 1000 \`\`\`bash
981 # 1. 移动到 approved 目录 1001 # 1. 移动到 approved 目录
982 mv docs/parse-audit/pending/${baseFileName}/${auditFileName} \\ 1002 mv docs/parse-audit/pending/${baseFileName}/${auditFileName} \\
983 docs/parse-audit/approved/ 1003 docs/parse-audit/approved/
984 1004
985 -# 2. 合并到正式配置 1005 +# 2. 手动复制"生成配置片段"到 src/config/plan-templates.js
986 -# 手动复制或使用工具合并到 src/config/plan-templates.js
987 -
988 -# 3. 删除待审核文件(可选)
989 -rm docs/parse-audit/pending/${baseFileName}/${auditFileName}
990 \`\`\` 1006 \`\`\`
991 1007
992 ### 需要修改 1008 ### 需要修改
...@@ -1402,6 +1418,189 @@ function rollbackConfigFile(backupFile) { ...@@ -1402,6 +1418,189 @@ function rollbackConfigFile(backupFile) {
1402 return true 1418 return true
1403 } 1419 }
1404 1420
1421 +/**
1422 + * 从审核文件应用配置到 plan-templates.js
1423 + *
1424 + * @description 读取审核 markdown 文件,提取配置代码,插入到配置文件中
1425 + * @param {string} auditFileName - 审核文件名(不含路径,如 "计划书模版4")
1426 + * @param {Object} options - 选项
1427 + * @param {boolean} options.dry_run - 是否仅预览
1428 + * @returns {Object} 应用结果
1429 + */
1430 +function applyAuditFile(auditFileName, options = {}) {
1431 + const PENDING_DIR = path.resolve(process.cwd(), 'docs/parse-audit/pending')
1432 + const APPROVED_DIR = path.resolve(process.cwd(), 'docs/parse-audit/approved')
1433 +
1434 + // 1. 查找审核文件
1435 + let auditFile = null
1436 + let sourceDir = null
1437 +
1438 + // 先在 pending 目录查找
1439 + const pendingDirs = fs.existsSync(PENDING_DIR) ? fs.readdirSync(PENDING_DIR) : []
1440 + for (const dir of pendingDirs) {
1441 + const dirPath = path.join(PENDING_DIR, dir)
1442 + if (fs.statSync(dirPath).isDirectory()) {
1443 + const files = fs.readdirSync(dirPath).filter(f => f.endsWith('.md'))
1444 + for (const file of files) {
1445 + // 匹配文件名或目录名
1446 + const normalizedName = dir.replace(/\s+/g, '').toLowerCase()
1447 + const normalizedInput = auditFileName.replace(/\s+/g, '').toLowerCase()
1448 + if (normalizedName.includes(normalizedInput) || normalizedInput.includes(normalizedName)) {
1449 + auditFile = path.join(dirPath, file)
1450 + sourceDir = PENDING_DIR
1451 + break
1452 + }
1453 + }
1454 + }
1455 + if (auditFile) break
1456 + }
1457 +
1458 + // 如果 pending 没找到,在 approved 目录查找
1459 + if (!auditFile && fs.existsSync(APPROVED_DIR)) {
1460 + const approvedFiles = fs.readdirSync(APPROVED_DIR).filter(f => f.endsWith('.md'))
1461 + for (const file of approvedFiles) {
1462 + // 从文件名提取产品名(格式:YYYY-MM-DD-产品名.md)
1463 + const match = file.match(/^\d{4}-\d{2}-\d{2}-(.+)\.md$/)
1464 + if (match) {
1465 + const normalizedName = match[1].replace(/\s+/g, '').toLowerCase()
1466 + const normalizedInput = auditFileName.replace(/\s+/g, '').toLowerCase()
1467 + if (normalizedName.includes(normalizedInput) || normalizedInput.includes(normalizedName)) {
1468 + auditFile = path.join(APPROVED_DIR, file)
1469 + sourceDir = APPROVED_DIR
1470 + break
1471 + }
1472 + }
1473 + }
1474 + }
1475 +
1476 + if (!auditFile) {
1477 + console.error("❌ 找不到审核文件: " + auditFileName)
1478 + console.log(" 搜索目录:")
1479 + console.log(" - docs/parse-audit/pending/")
1480 + console.log(" - docs/parse-audit/approved/")
1481 + return { ok: false, reason: 'file_not_found' }
1482 + }
1483 +
1484 + console.log("\n📄 找到审核文件: " + auditFile)
1485 +
1486 + // 2. 读取审核文件内容
1487 + const content = fs.readFileSync(auditFile, 'utf-8')
1488 +
1489 + // 3. 提取配置代码片段
1490 + const configMatch = content.match(/## 🧩 生成配置片段\s*\n+```javascript\s*\n([\s\S]*?)```/)
1491 + if (!configMatch) {
1492 + console.error("❌ 无法从审核文件中提取配置代码")
1493 + return { ok: false, reason: 'config_not_found' }
1494 + }
1495 +
1496 + const configCode = configMatch[1].trim()
1497 + console.log("\n📝 提取的配置代码:")
1498 + console.log("-".repeat(40))
1499 + console.log(configCode)
1500 + console.log("-".repeat(40))
1501 +
1502 + // 4. 提取 form_sn 用于去重检查
1503 + const formSnMatch = configCode.match(/'([^']+)':\s*\{/)
1504 + const formSn = formSnMatch ? formSnMatch[1] : null
1505 +
1506 + if (!formSn) {
1507 + console.error("❌ 无法从配置代码中提取 form_sn")
1508 + return { ok: false, reason: 'form_sn_not_found' }
1509 + }
1510 +
1511 + console.log("\n🔑 form_sn: " + formSn)
1512 +
1513 + // 5. 读取现有配置文件
1514 + const existingContent = fs.readFileSync(CONFIG_FILE, 'utf-8')
1515 +
1516 + // 检查是否已存在
1517 + if (existingContent.includes(`'${formSn}':`)) {
1518 + console.error("❌ 配置文件中已存在 form_sn: " + formSn)
1519 + console.log(" 如需更新,请先手动删除旧配置")
1520 + return { ok: false, reason: 'duplicate', formSn }
1521 + }
1522 +
1523 + // 6. 找到插入位置(PLAN_TEMPLATES 对象的结束位置)
1524 + // 查找最后一个产品配置的结束位置
1525 + const insertPattern = /(\n\s*'\w+[^']+':\s*\{[\s\S]*?\n\s*\}\s*,?\s*)(\n\})/
1526 + const match = existingContent.match(insertPattern)
1527 +
1528 + if (!match) {
1529 + console.error("❌ 无法定位插入位置")
1530 + return { ok: false, reason: 'insert_not_found' }
1531 + }
1532 +
1533 + // 7. 构建新配置(确保有逗号)
1534 + let newConfigEntry = configCode
1535 + // 确保配置以逗号结尾
1536 + if (!newConfigEntry.trimEnd().endsWith(',')) {
1537 + newConfigEntry = newConfigEntry.trimEnd() + ','
1538 + }
1539 +
1540 + // 8. 插入配置
1541 + const insertPosition = match.index + match[1].length
1542 + const updatedContent =
1543 + existingContent.slice(0, insertPosition) +
1544 + '\n\n' +
1545 + newConfigEntry +
1546 + existingContent.slice(insertPosition)
1547 +
1548 + if (options.dry_run) {
1549 + console.log("\n🧪 dry-run 模式,变更预览:")
1550 + console.log("-".repeat(40))
1551 + console.log("将插入以下配置:")
1552 + console.log(newConfigEntry)
1553 + console.log("-".repeat(40))
1554 + return { ok: true, dry_run: true, formSn }
1555 + }
1556 +
1557 + // 9. 备份并写入
1558 + let backupFile = null
1559 + if (fs.existsSync(CONFIG_FILE)) {
1560 + ensureDir(BACKUP_DIR)
1561 + backupFile = path.join(BACKUP_DIR, `plan-templates.backup.${Date.now()}.js`)
1562 + fs.copyFileSync(CONFIG_FILE, backupFile)
1563 + console.log("\n💾 已备份到: " + backupFile)
1564 + }
1565 +
1566 + writeFile(CONFIG_FILE, updatedContent)
1567 + console.log("\n✅ 配置已更新: " + CONFIG_FILE)
1568 +
1569 + writeBackupLog({
1570 + action: 'apply_audit',
1571 + backup_file: backupFile,
1572 + target_file: CONFIG_FILE,
1573 + audit_file: auditFile,
1574 + form_sn: formSn,
1575 + at: new Date().toISOString()
1576 + })
1577 +
1578 + // 10. 移动审核文件到 approved 目录(如果是从 pending 来的)
1579 + if (sourceDir === PENDING_DIR) {
1580 + ensureDir(APPROVED_DIR)
1581 + const fileName = path.basename(auditFile)
1582 + const approvedPath = path.join(APPROVED_DIR, fileName)
1583 +
1584 + // 检查目标是否已存在
1585 + if (fs.existsSync(approvedPath)) {
1586 + console.log("⚠️ approved 目录已存在同名文件,跳过移动")
1587 + } else {
1588 + fs.renameSync(auditFile, approvedPath)
1589 + console.log("📁 审核文件已移动到: " + approvedPath)
1590 +
1591 + // 删除空的 pending 子目录
1592 + const pendingSubDir = path.dirname(auditFile)
1593 + const remainingFiles = fs.readdirSync(pendingSubDir).filter(f => !f.startsWith('.'))
1594 + if (remainingFiles.length === 0) {
1595 + fs.rmdirSync(pendingSubDir)
1596 + console.log("🗑️ 已删除空目录: " + pendingSubDir)
1597 + }
1598 + }
1599 + }
1600 +
1601 + return { ok: true, formSn, backupFile }
1602 +}
1603 +
1405 function updateConfigFile(newConfigs, options = {}) { 1604 function updateConfigFile(newConfigs, options = {}) {
1406 console.log("\n" + "=".repeat(60)) 1605 console.log("\n" + "=".repeat(60))
1407 console.log("📝 更新配置文件: " + CONFIG_FILE) 1606 console.log("📝 更新配置文件: " + CONFIG_FILE)
...@@ -1563,9 +1762,16 @@ async function main() { ...@@ -1563,9 +1762,16 @@ async function main() {
1563 const listMode = args.includes('--list') 1762 const listMode = args.includes('--list')
1564 const fileMode = args.find(arg => arg.startsWith('--file=')) 1763 const fileMode = args.find(arg => arg.startsWith('--file='))
1565 const writeMode = args.includes('--write-config') 1764 const writeMode = args.includes('--write-config')
1566 - const dryRunMode = args.includes('--dry-run') || !writeMode
1567 const rollbackMode = args.find(arg => arg.startsWith('--rollback=')) 1765 const rollbackMode = args.find(arg => arg.startsWith('--rollback='))
1568 const statusMode = args.includes('--status') 1766 const statusMode = args.includes('--status')
1767 + const applyMode = args.find(arg => arg.startsWith('--apply='))
1768 +
1769 + // dry-run 逻辑:
1770 + // 1. 如果显式指定 --dry-run,则 dry-run
1771 + // 2. 如果是 apply 模式,默认不 dry-run(除非显式指定)
1772 + // 3. 如果是解析模式,默认 dry-run(除非显式指定 --write-config)
1773 + const explicitDryRun = args.includes('--dry-run')
1774 + const dryRunMode = applyMode ? explicitDryRun : (!writeMode && !explicitDryRun || explicitDryRun)
1569 1775
1570 // 检查解析器选择 1776 // 检查解析器选择
1571 const parserModeArg = args.find(arg => arg.startsWith('--parser=')) 1777 const parserModeArg = args.find(arg => arg.startsWith('--parser='))
...@@ -1586,6 +1792,11 @@ async function main() { ...@@ -1586,6 +1792,11 @@ async function main() {
1586 if (rollbackMode) { 1792 if (rollbackMode) {
1587 const backupFile = rollbackMode.split('=')[1] 1793 const backupFile = rollbackMode.split('=')[1]
1588 rollbackConfigFile(backupFile) 1794 rollbackConfigFile(backupFile)
1795 + } else if (applyMode) {
1796 + // 从审核文件应用配置
1797 + const auditFileName = applyMode.split('=')[1]
1798 + const applyOptions = { dry_run: dryRunMode }
1799 + applyAuditFile(auditFileName, applyOptions)
1589 } else if (listMode) { 1800 } else if (listMode) {
1590 // 列出模式 1801 // 列出模式
1591 const docs = getDocsToParse() 1802 const docs = getDocsToParse()
......
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
16 * - GC宏摯家傳承保險計劃- 性別, 年齡, 出生年月日 16 * - GC宏摯家傳承保險計劃- 性別, 年齡, 出生年月日
17 * - FA 宏浚傳承保障計劃 17 * - FA 宏浚傳承保障計劃
18 * - LV2 赤霞珠終身壽險計劃2基本人壽保障選項 18 * - LV2 赤霞珠終身壽險計劃2基本人壽保障選項
19 + * - LV3 长宁終身壽險計劃3
19 */ 20 */
20 const PRODUCT_TITLE_PATTERNS = [ 21 const PRODUCT_TITLE_PATTERNS = [
21 // 产品代码 + 产品名称 + 可选后缀 22 // 产品代码 + 产品名称 + 可选后缀
...@@ -31,14 +32,17 @@ const PRODUCT_TITLE_PATTERNS = [ ...@@ -31,14 +32,17 @@ const PRODUCT_TITLE_PATTERNS = [
31 /^([^\n]{2,30}?(?:計劃|计划|保障|保险|壽險|壽险)[^\n]*)/gm, 32 /^([^\n]{2,30}?(?:計劃|计划|保障|保险|壽險|壽险)[^\n]*)/gm,
32 33
33 // 产品代码开头的行 34 // 产品代码开头的行
34 - /^([A-Z]{2,4}\d?)\s*[-:]\s*([^\n]+)/gm 35 + /^([A-Z]{2,4}\d?)\s*[-:]\s*([^\n]+)/gm,
36 +
37 + // 新增:产品代码 + 产品名称 + 数字后缀(如 "LV3 长宁終身壽險計劃3")
38 + /^([A-Z]{2,3}\d?)\s+([^\n]{2,25}?(?:計劃|计划|壽險|壽险)\d?)/gm
35 ] 39 ]
36 40
37 /** 41 /**
38 * 产品代码前缀列表(用于优先匹配) 42 * 产品代码前缀列表(用于优先匹配)
39 */ 43 */
40 const PRODUCT_CODE_PREFIXES = [ 44 const PRODUCT_CODE_PREFIXES = [
41 - 'GS', 'GC', 'FA', 'LV2', 'LV', 'CR', 'HR', 'PR', 'SR', 45 + 'GS', 'GC', 'FA', 'LV2', 'LV3', 'LV', 'CR', 'HR', 'PR', 'SR',
42 'TR', 'UR', 'WR', 'XR', 'YR', 'ZR' 46 'TR', 'UR', 'WR', 'XR', 'YR', 'ZR'
43 ] 47 ]
44 48
...@@ -62,10 +66,11 @@ export function detectProductCount(content) { ...@@ -62,10 +66,11 @@ export function detectProductCount(content) {
62 export function findProductTitles(content) { 66 export function findProductTitles(content) {
63 const products = [] 67 const products = []
64 const seenCodes = new Set() 68 const seenCodes = new Set()
69 + const seenNames = new Set()
65 70
66 // 策略1: 优先匹配产品代码前缀 71 // 策略1: 优先匹配产品代码前缀
67 for (const prefix of PRODUCT_CODE_PREFIXES) { 72 for (const prefix of PRODUCT_CODE_PREFIXES) {
68 - // 匹配 "GS宏摯傳承保障計劃" 或 "GS 宏摯傳承保障計劃" 73 + // 匹配 "GS宏摯傳承保障計劃" 或 "GS 宏摯傳承保障計劃" 或 "LV3 长宁終身壽險計劃3"
69 const regex = new RegExp( 74 const regex = new RegExp(
70 `^(${prefix}\\d?)\\s*([\\u4e00-\\u9fa5]+(?:計劃|计划|保障|保险|壽險|壽险)[^\\n]*)`, 75 `^(${prefix}\\d?)\\s*([\\u4e00-\\u9fa5]+(?:計劃|计划|保障|保险|壽險|壽险)[^\\n]*)`,
71 'gm' 76 'gm'
...@@ -76,9 +81,11 @@ export function findProductTitles(content) { ...@@ -76,9 +81,11 @@ export function findProductTitles(content) {
76 const code = match[1] 81 const code = match[1]
77 const name = match[2].trim() 82 const name = match[2].trim()
78 83
79 - // 去重 84 + // 去重(基于代码或名称)
80 - if (seenCodes.has(code)) continue 85 + const nameKey = name.replace(/\s+/g, '').toLowerCase()
86 + if (seenCodes.has(code) || seenNames.has(nameKey)) continue
81 seenCodes.add(code) 87 seenCodes.add(code)
88 + seenNames.add(nameKey)
82 89
83 products.push({ 90 products.push({
84 index: match.index, 91 index: match.index,
...@@ -99,14 +106,51 @@ export function findProductTitles(content) { ...@@ -99,14 +106,51 @@ export function findProductTitles(content) {
99 const fullTitle = match[0].trim() 106 const fullTitle = match[0].trim()
100 if (fullTitle.length < 5) continue // 过滤太短的匹配 107 if (fullTitle.length < 5) continue // 过滤太短的匹配
101 108
109 + const code = match[1] || null
110 + const name = match[2] || fullTitle
111 +
112 + // 去重
113 + const nameKey = name.replace(/\s+/g, '').toLowerCase()
114 + if (seenNames.has(nameKey)) continue
115 + if (code) seenCodes.add(code)
116 + seenNames.add(nameKey)
117 +
118 + products.push({
119 + index: match.index,
120 + code,
121 + name,
122 + fullTitle
123 + })
124 + }
125 + }
126 +
127 + // 策略3: 新增 - 识别包含"计划"但不包含产品代码的行(纯计划书名称)
128 + // 适用于标题如 "宏挚传承保障计划" 或 "长宁终身寿险计划3"
129 + if (products.length === 0) {
130 + const planNameRegex = /^([^\n]{2,30}?(?:計劃|计划)[^\n]*)/gm
131 + let match
132 +
133 + while ((match = planNameRegex.exec(content)) !== null) {
134 + const fullTitle = match[1].trim()
135 +
136 + // 排除太短或包含其他关键词的行
137 + if (fullTitle.length < 5 || fullTitle.includes('選項') || fullTitle.includes('选项')) continue
138 +
139 + // 检查是否是产品名称(通常包含"保障"、"保险"、"寿险"等关键词)
140 + if (/(?:保障|保险|壽險|壽险|传承|家传)/.test(fullTitle)) {
141 + const nameKey = fullTitle.replace(/\s+/g, '').toLowerCase()
142 + if (!seenNames.has(nameKey)) {
143 + seenNames.add(nameKey)
102 products.push({ 144 products.push({
103 index: match.index, 145 index: match.index,
104 - code: match[1] || null, 146 + code: null,
105 - name: match[2] || fullTitle, 147 + name: fullTitle.split(/[-—::]/)[0].trim(), // 移除后缀说明
106 fullTitle 148 fullTitle
107 }) 149 })
108 } 150 }
109 } 151 }
152 + }
153 + }
110 154
111 // 按出现位置排序 155 // 按出现位置排序
112 products.sort((a, b) => a.index - b.index) 156 products.sort((a, b) => a.index - b.index)
......
This diff is collapsed. Click to expand it.
...@@ -368,6 +368,24 @@ export const PLAN_TEMPLATES = { ...@@ -368,6 +368,24 @@ export const PLAN_TEMPLATES = {
368 submit_mapping: savingsSubmitMapping 368 submit_mapping: savingsSubmitMapping
369 } 369 }
370 }, 370 },
371 +
372 + /**
373 + * 长宁終身壽險計劃3
374 + * @added 2026-02-15T06:30:03.691Z
375 + * @source docs/to-parse/计划书模版4.docx
376 + */
377 + 'life-insurance-3-d8fde07d': {
378 + name: '长宁終身壽險計劃3',
379 + component: 'LifeInsuranceTemplate',
380 + config: {
381 + currency: 'USD',
382 + payment_periods: ["5年","12年","15年","20年"],
383 + age_range: { min: 0, max: 75 },
384 + insurance_period: '终身',
385 + form_schema: protectionFormSchema,
386 + submit_mapping: baseSubmitMapping
387 + }
388 + }
371 } 389 }
372 390
373 /** 391 /**
......
...@@ -415,9 +415,11 @@ export async function mockProductListAPI(params) { ...@@ -415,9 +415,11 @@ export async function mockProductListAPI(params) {
415 const list = [] 415 const list = []
416 const startIndex = page * limit 416 const startIndex = page * limit
417 417
418 - // 🔧 测试商品:第一页第一位固定为储蓄产品(form_sn:savings-product-30b41aae) 418 + // 🔧 测试商品:第一页前两位固定为测试产品
419 if (page === 0) { 419 if (page === 0) {
420 const testCategory = PRODUCT_CATEGORIES.find(c => parseInt(c.id) === 1) 420 const testCategory = PRODUCT_CATEGORIES.find(c => parseInt(c.id) === 1)
421 +
422 + // 测试商品1: 储蓄产品
421 const testProduct1 = { 423 const testProduct1 = {
422 id: 'savings-2-148b3acd', 424 id: 'savings-2-148b3acd',
423 product_name: '测试计划书-智享未来2(form_sn:savings-2-148b3acd)', 425 product_name: '测试计划书-智享未来2(form_sn:savings-2-148b3acd)',
...@@ -432,19 +434,38 @@ export async function mockProductListAPI(params) { ...@@ -432,19 +434,38 @@ export async function mockProductListAPI(params) {
432 _test_note: 'form_sn:savings-2-148b3acd' 434 _test_note: 'form_sn:savings-2-148b3acd'
433 } 435 }
434 436
435 - // 检查分类和关键词过滤 437 + // 测试商品2: 人寿保险产品
438 + const testProduct2 = {
439 + id: 'life-insurance-3-d8fde07d',
440 + product_name: '测试计划书-人生无忧3(form_sn:life-insurance-3-d8fde07d)',
441 + cover_image: 'https://picsum.photos/seed/life-insurance-3-d8fde07d/400/300',
442 + recommend: 'hot',
443 + form_sn: 'life-insurance-3-d8fde07d', // ✅ 关键字段:对应真实 API 的 form_sn
444 + created_time: new Date().toISOString(),
445 + categories: [testCategory], // ✅ 符合真实 API 结构:categories 是数组
446 + tags: [{ id: '1', name: '热销', bg_color: '#FEE2E2', text_color: '#DC2626' }],
447 + // 测试标识(不影响业务逻辑)
448 + _test: true,
449 + _test_note: 'form_sn:life-insurance-3-d8fde07d'
450 + }
451 +
452 + // 检查分类和关键词过滤,依次添加测试商品
453 + const testProducts = [testProduct1, testProduct2]
454 +
455 + testProducts.forEach((testProduct, index) => {
436 let shouldInclude = true 456 let shouldInclude = true
437 - if (cid && !testProduct1.categories.some(c => parseInt(c.id) === parseInt(cid))) { 457 + if (cid && !testProduct.categories.some(c => parseInt(c.id) === parseInt(cid))) {
438 shouldInclude = false 458 shouldInclude = false
439 } 459 }
440 - if (keyword && !testProduct1.product_name.includes(keyword)) { 460 + if (keyword && !testProduct.product_name.includes(keyword)) {
441 shouldInclude = false 461 shouldInclude = false
442 } 462 }
443 463
444 if (shouldInclude) { 464 if (shouldInclude) {
445 - list.push(testProduct1) 465 + list.push(testProduct)
446 - console.log('[Mock] listAPI - 测试商品已置顶: form_sn=savings-2-148b3acd') 466 + console.log(`[Mock] listAPI - 测试商品${index + 1}已置顶: form_sn=${testProduct.form_sn}`)
447 } 467 }
468 + })
448 } 469 }
449 470
450 for (let i = 0; i < limit; i++) { 471 for (let i = 0; i < limit; i++) {
......