hookehuyr

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

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

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
......@@ -5,3 +5,4 @@
{"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"}
{"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"}
{"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"}
{"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"}
......
......@@ -28,3 +28,11 @@
{"at":"2026-02-15T02:19:44.455Z","mode":"batch","options":{"dry_run":true},"summary":{"total_docs":1,"total_products":1,"success":1,"failed":0,"duration_ms":39,"success_list":[{"form_sn":"savings-3-8f4f27ad","product_name":"计划书模版3","file":"计划书模版3.docx"}],"failed_list":[],"total":1},"change_summary":{"ok":true,"dry_run":true,"updated_count":1,"form_sn_list":["savings-3-8f4f27ad"],"conflicts":[],"reason":null,"diff_preview":"--- plan-templates.js\n+++ plan-templates.js\n+ /**\n+ * 计划书模版3\n+ * @added 2026-02-15T02:19:44.438Z\n+ * @source docs/to-parse/计划书模版3.docx\n+ */\n+ 'savings-3-8f4f27ad': {\n+ name: '计划书模版3',\n+ component: 'SavingsTemplate',\n+ category: 'savings',\n+ config: {\n+ currency: 'USD',\n+ payment_periods: [\"5年\",\"8年\",\"12年\",\"15年\"],\n+ age_range: { min: 0, max: 75 },\n+ insurance_period: '终身',\n+ withdrawal_plan: {\n+ enabled: true,\n+ currencies: ['HKD', 'USD', 'CNY'],\n+ default_currency: 'USD',\n+ withdrawal_modes: [\"年龄指定金额\",\"最高固定金额\"],\n+ withdrawal_periods: [\"1年\",\"3年\",\"5年\",\"10年\"]\n+ },\n+ form_schema: savingsFormSchema,\n+ submit_mapping: savingsSubmitMapping\n+ }\n+ }"}}
{"at":"2026-02-15T02:20:31.001Z","mode":"batch","options":{"dry_run":true},"summary":{"total_docs":1,"total_products":4,"success":4,"failed":0,"duration_ms":47,"success_list":[{"form_sn":"savings-product-ef3dd50b","product_name":"宏摯傳承保障計劃 - 性別, 年齡, 出生年月日","file":"计划书模版2.docx"},{"form_sn":"savings-product-aaaa60f8","product_name":"宏摯家傳承保險計劃- 性別, 年齡, 出生年月日","file":"计划书模版2.docx"},{"form_sn":"savings-product-d1581522","product_name":"宏浚傳承保障計劃","file":"计划书模版2.docx"},{"form_sn":"savings-2-031c1237","product_name":"赤霞珠終身壽險計劃2基本人壽保障選項","file":"计划书模版2.docx"}],"failed_list":[],"total":4},"change_summary":{"ok":true,"dry_run":true,"updated_count":4,"form_sn_list":["savings-product-ef3dd50b","savings-product-aaaa60f8","savings-product-d1581522","savings-2-031c1237"],"conflicts":[],"reason":null,"diff_preview":"--- plan-templates.js\n+++ plan-templates.js\n+ /**\n+ * 宏摯傳承保障計劃 - 性別, 年齡, 出生年月日\n+ * @added 2026-02-15T02:20:30.982Z\n+ * @source docs/to-parse/计划书模版2.docx\n+ */\n+ 'savings-product-ef3dd50b': {\n+ name: '宏摯傳承保障計劃 - 性別, 年齡, 出生年月日',\n+ component: 'SavingsTemplate',\n+ category: 'savings',\n+ config: {\n+ currency: 'USD',\n+ payment_periods: [\"整付\",\"3年\",\"5年\",\"10年\",\"15年\"],\n+ age_range: { min: 0, max: 75 },\n+ insurance_period: '终身',\n+ withdrawal_plan: {\n+ enabled: true,\n+ currencies: ['HKD', 'USD', 'CNY'],\n+ default_currency: 'USD',\n+ withdrawal_modes: [\"最高固定提取金额\"],\n+ withdrawal_periods: [\"1年\",\"3年\",\"5年\",\"10年\"]\n+ },\n+ form_schema: savingsFormSchema,\n+ submit_mapping: savingsSubmitMapping\n+ }\n+ },\n+ \n+ /**\n+ * 宏摯家傳承保險計劃- 性別, 年齡, 出生年月日\n+ * @added 2026-02-15T02:20:30.997Z\n+ * @source docs/to-parse/计划书模版2.docx\n+ */\n+ 'savings-product-aaaa60f8': {\n+ name: '宏摯家傳承保險計劃- 性別, 年齡, 出生年月日',\n+ component: 'SavingsTemplate',\n+ category: 'savings',\n+ config: {\n+ currency: 'USD',\n+ payment_periods: [\"整付\",\"3年\",\"5年\"],\n+ age_range: { min: 0, max: 75 },\n+ insurance_period: '终身',\n+ withdrawal_plan: {\n+ enabled: true,\n+ currencies: ['HKD', 'USD', 'CNY'],\n+ default_currency: 'USD',\n+ withdrawal_modes: [\"年龄指定金额\",\"最高固定金额\"],\n+ withdrawal_periods: [\"1年\",\"3年\",\"5年\",\"10年\"]\n+ },\n+ form_schema: savingsFormSchema,\n+ submit_mapping: savingsSubmitMapping\n+ }\n+ },\n+ \n+ /**\n+ * 宏浚傳承保障計劃\n+ * @added 2026-02-15T02:20:30.997Z\n+ * @source docs/to-parse/计划书模版2.docx\n+ */\n+ 'savings-product-d1581522': {"}}
{"at":"2026-02-15T03:18:47.647Z","mode":"batch","options":{"dry_run":true},"summary":{"total_docs":1,"total_products":4,"success":4,"failed":0,"duration_ms":54,"success_list":[{"form_sn":"savings-product-ef3dd50b","product_name":"宏摯傳承保障計劃 - 性別, 年齡, 出生年月日","file":"计划书模版2.docx"},{"form_sn":"savings-product-aaaa60f8","product_name":"宏摯家傳承保險計劃- 性別, 年齡, 出生年月日","file":"计划书模版2.docx"},{"form_sn":"savings-product-d1581522","product_name":"宏浚傳承保障計劃","file":"计划书模版2.docx"},{"form_sn":"savings-2-031c1237","product_name":"赤霞珠終身壽險計劃2基本人壽保障選項","file":"计划书模版2.docx"}],"failed_list":[],"total":4},"change_summary":{"ok":true,"dry_run":true,"updated_count":4,"form_sn_list":["savings-product-ef3dd50b","savings-product-aaaa60f8","savings-product-d1581522","savings-2-031c1237"],"conflicts":[],"reason":null,"diff_preview":"--- plan-templates.js\n+++ plan-templates.js\n+ /**\n+ * 宏摯傳承保障計劃 - 性別, 年齡, 出生年月日\n+ * @added 2026-02-15T03:18:47.616Z\n+ * @source docs/to-parse/计划书模版2.docx\n+ */\n+ 'savings-product-ef3dd50b': {\n+ name: '宏摯傳承保障計劃 - 性別, 年齡, 出生年月日',\n+ component: 'SavingsTemplate',\n+ category: 'savings',\n+ config: {\n+ currency: 'USD',\n+ payment_periods: [\"整付\",\"3年\",\"5年\",\"10年\",\"15年\"],\n+ age_range: { min: 0, max: 75 },\n+ insurance_period: '终身',\n+ withdrawal_plan: {\n+ enabled: true,\n+ currencies: ['HKD', 'USD', 'CNY'],\n+ default_currency: 'USD',\n+ withdrawal_modes: [\"最高固定提取金额\"],\n+ withdrawal_periods: [\"1年\",\"3年\",\"5年\",\"10年\"]\n+ },\n+ form_schema: savingsFormSchema,\n+ submit_mapping: savingsSubmitMapping\n+ }\n+ },\n+ \n+ /**\n+ * 宏摯家傳承保險計劃- 性別, 年齡, 出生年月日\n+ * @added 2026-02-15T03:18:47.644Z\n+ * @source docs/to-parse/计划书模版2.docx\n+ */\n+ 'savings-product-aaaa60f8': {\n+ name: '宏摯家傳承保險計劃- 性別, 年齡, 出生年月日',\n+ component: 'SavingsTemplate',\n+ category: 'savings',\n+ config: {\n+ currency: 'USD',\n+ payment_periods: [\"整付\",\"3年\",\"5年\"],\n+ age_range: { min: 0, max: 75 },\n+ insurance_period: '终身',\n+ withdrawal_plan: {\n+ enabled: true,\n+ currencies: ['HKD', 'USD', 'CNY'],\n+ default_currency: 'USD',\n+ withdrawal_modes: [\"年龄指定金额\",\"最高固定金额\"],\n+ withdrawal_periods: [\"1年\",\"3年\",\"5年\",\"10年\"]\n+ },\n+ form_schema: savingsFormSchema,\n+ submit_mapping: savingsSubmitMapping\n+ }\n+ },\n+ \n+ /**\n+ * 宏浚傳承保障計劃\n+ * @added 2026-02-15T03:18:47.644Z\n+ * @source docs/to-parse/计划书模版2.docx\n+ */\n+ 'savings-product-d1581522': {"}}
{"at":"2026-02-15T05:43:33.427Z","mode":"single","options":{"dry_run":true},"summary":{"total_docs":1,"total_products":1,"success":1,"failed":0,"duration_ms":39,"success_list":[{"form_sn":"savings-4-a204daaf","product_name":"计划书模版4","file":"计划书模版4.docx"}],"failed_list":[],"total":1},"change_summary":{"ok":true,"dry_run":true,"updated_count":1,"form_sn_list":["savings-4-a204daaf"],"conflicts":[],"reason":null,"diff_preview":"--- plan-templates.js\n+++ plan-templates.js\n+ /**\n+ * 计划书模版4\n+ * @added 2026-02-15T05:43:33.409Z\n+ * @source docs/to-parse/计划书模版4.docx\n+ */\n+ 'savings-4-a204daaf': {\n+ name: '计划书模版4',\n+ component: 'SavingsTemplate',\n+ category: 'savings',\n+ config: {\n+ currency: 'USD',\n+ payment_periods: [\"5年\",\"12年\",\"15年\"],\n+ age_range: { min: 0, max: 75 },\n+ insurance_period: '终身',\n+ withdrawal_plan: {\n+ enabled: true,\n+ currencies: ['HKD', 'USD', 'CNY'],\n+ default_currency: 'USD',\n+ withdrawal_modes: [\"年龄指定金额\",\"最高固定金额\"],\n+ withdrawal_periods: [\"1年\",\"3年\",\"5年\",\"10年\"]\n+ },\n+ form_schema: savingsFormSchema,\n+ submit_mapping: savingsSubmitMapping\n+ }\n+ }"}}
{"at":"2026-02-15T05:45:13.034Z","mode":"batch","options":{"dry_run":true},"summary":{"total_docs":1,"total_products":1,"success":1,"failed":0,"duration_ms":39,"success_list":[{"form_sn":"savings-4-a204daaf","product_name":"计划书模版4","file":"计划书模版4.docx"}],"failed_list":[],"total":1},"change_summary":{"ok":true,"dry_run":true,"updated_count":1,"form_sn_list":["savings-4-a204daaf"],"conflicts":[],"reason":null,"diff_preview":"--- plan-templates.js\n+++ plan-templates.js\n+ /**\n+ * 计划书模版4\n+ * @added 2026-02-15T05:45:13.016Z\n+ * @source docs/to-parse/计划书模版4.docx\n+ */\n+ 'savings-4-a204daaf': {\n+ name: '计划书模版4',\n+ component: 'SavingsTemplate',\n+ category: 'savings',\n+ config: {\n+ currency: 'USD',\n+ payment_periods: [\"5年\",\"12年\",\"15年\"],\n+ age_range: { min: 0, max: 75 },\n+ insurance_period: '终身',\n+ withdrawal_plan: {\n+ enabled: true,\n+ currencies: ['HKD', 'USD', 'CNY'],\n+ default_currency: 'USD',\n+ withdrawal_modes: [\"年龄指定金额\",\"最高固定金额\"],\n+ withdrawal_periods: [\"1年\",\"3年\",\"5年\",\"10年\"]\n+ },\n+ form_schema: savingsFormSchema,\n+ submit_mapping: savingsSubmitMapping\n+ }\n+ }"}}
{"at":"2026-02-15T06:08:21.190Z","mode":"single","options":{"dry_run":true},"summary":{"total_docs":1,"total_products":1,"success":1,"failed":0,"duration_ms":51,"success_list":[{"form_sn":"life-insurance-3-d8fde07d","product_name":"长宁終身壽險計劃3","file":"计划书模版4.docx"}],"failed_list":[],"total":1},"change_summary":{"ok":true,"dry_run":true,"updated_count":1,"form_sn_list":["life-insurance-3-d8fde07d"],"conflicts":[],"reason":null,"diff_preview":"--- plan-templates.js\n+++ plan-templates.js\n+ /**\n+ * 长宁終身壽險計劃3\n+ * @added 2026-02-15T06:08:21.159Z\n+ * @source docs/to-parse/计划书模版4.docx\n+ */\n+ 'life-insurance-3-d8fde07d': {\n+ name: '长宁終身壽險計劃3',\n+ component: 'LifeInsuranceTemplate',\n+ config: {\n+ currency: 'USD',\n+ payment_periods: [\"5年\",\"12年\",\"15年\"],\n+ age_range: { min: 0, max: 75 },\n+ insurance_period: '终身',\n+ form_schema: protectionFormSchema,\n+ submit_mapping: baseSubmitMapping\n+ }\n+ }"}}
{"at":"2026-02-15T06:12:50.918Z","mode":"single","options":{"dry_run":true},"summary":{"total_docs":1,"total_products":1,"success":1,"failed":0,"duration_ms":39,"success_list":[{"form_sn":"life-insurance-3-d8fde07d","product_name":"长宁終身壽險計劃3","file":"计划书模版4.docx"}],"failed_list":[],"total":1},"change_summary":{"ok":true,"dry_run":true,"updated_count":1,"form_sn_list":["life-insurance-3-d8fde07d"],"conflicts":[],"reason":null,"diff_preview":"--- plan-templates.js\n+++ plan-templates.js\n+ /**\n+ * 长宁終身壽險計劃3\n+ * @added 2026-02-15T06:12:50.901Z\n+ * @source docs/to-parse/计划书模版4.docx\n+ */\n+ 'life-insurance-3-d8fde07d': {\n+ name: '长宁終身壽險計劃3',\n+ component: 'LifeInsuranceTemplate',\n+ config: {\n+ currency: 'USD',\n+ payment_periods: [\"5年\",\"12年\",\"15年\",\"终身\"],\n+ age_range: { min: 0, max: 75 },\n+ insurance_period: '终身',\n+ form_schema: protectionFormSchema,\n+ submit_mapping: baseSubmitMapping\n+ }\n+ }"}}
{"at":"2026-02-15T06:13:11.114Z","mode":"single","options":{"dry_run":true},"summary":{"total_docs":1,"total_products":1,"success":1,"failed":0,"duration_ms":39,"success_list":[{"form_sn":"life-insurance-3-d8fde07d","product_name":"长宁終身壽險計劃3","file":"计划书模版4.docx"}],"failed_list":[],"total":1},"change_summary":{"ok":true,"dry_run":true,"updated_count":1,"form_sn_list":["life-insurance-3-d8fde07d"],"conflicts":[],"reason":null,"diff_preview":"--- plan-templates.js\n+++ plan-templates.js\n+ /**\n+ * 长宁終身壽險計劃3\n+ * @added 2026-02-15T06:13:11.097Z\n+ * @source docs/to-parse/计划书模版4.docx\n+ */\n+ 'life-insurance-3-d8fde07d': {\n+ name: '长宁終身壽險計劃3',\n+ component: 'LifeInsuranceTemplate',\n+ config: {\n+ currency: 'USD',\n+ payment_periods: [\"5年\",\"12年\",\"15年\",\"终身\"],\n+ age_range: { min: 0, max: 75 },\n+ insurance_period: '终身',\n+ form_schema: protectionFormSchema,\n+ submit_mapping: baseSubmitMapping\n+ }\n+ }"}}
{"at":"2026-02-15T06:17:26.826Z","mode":"single","options":{"dry_run":true},"summary":{"total_docs":1,"total_products":1,"success":1,"failed":0,"duration_ms":38,"success_list":[{"form_sn":"life-insurance-3-d8fde07d","product_name":"长宁終身壽險計劃3","file":"计划书模版4.docx"}],"failed_list":[],"total":1},"change_summary":{"ok":true,"dry_run":true,"updated_count":1,"form_sn_list":["life-insurance-3-d8fde07d"],"conflicts":[],"reason":null,"diff_preview":"--- plan-templates.js\n+++ plan-templates.js\n+ /**\n+ * 长宁終身壽險計劃3\n+ * @added 2026-02-15T06:17:26.808Z\n+ * @source docs/to-parse/计划书模版4.docx\n+ */\n+ 'life-insurance-3-d8fde07d': {\n+ name: '长宁終身壽險計劃3',\n+ component: 'LifeInsuranceTemplate',\n+ config: {\n+ currency: 'USD',\n+ payment_periods: [\"5年\",\"12年\",\"15年\",\"20年\"],\n+ age_range: { min: 0, max: 75 },\n+ insurance_period: '终身',\n+ form_schema: protectionFormSchema,\n+ submit_mapping: baseSubmitMapping\n+ }\n+ }"}}
{"at":"2026-02-15T06:17:45.723Z","mode":"single","options":{"dry_run":true},"summary":{"total_docs":1,"total_products":1,"success":1,"failed":0,"duration_ms":38,"success_list":[{"form_sn":"life-insurance-3-d8fde07d","product_name":"长宁終身壽險計劃3","file":"计划书模版4.docx"}],"failed_list":[],"total":1},"change_summary":{"ok":true,"dry_run":true,"updated_count":1,"form_sn_list":["life-insurance-3-d8fde07d"],"conflicts":[],"reason":null,"diff_preview":"--- plan-templates.js\n+++ plan-templates.js\n+ /**\n+ * 长宁終身壽險計劃3\n+ * @added 2026-02-15T06:17:45.705Z\n+ * @source docs/to-parse/计划书模版4.docx\n+ */\n+ 'life-insurance-3-d8fde07d': {\n+ name: '长宁終身壽險計劃3',\n+ component: 'LifeInsuranceTemplate',\n+ config: {\n+ currency: 'USD',\n+ payment_periods: [\"5年\",\"12年\",\"15年\",\"20年\"],\n+ age_range: { min: 0, max: 75 },\n+ insurance_period: '终身',\n+ form_schema: protectionFormSchema,\n+ submit_mapping: baseSubmitMapping\n+ }\n+ }"}}
{"at":"2026-02-15T06:30:03.709Z","mode":"single","options":{"dry_run":false},"summary":{"total_docs":1,"total_products":1,"success":1,"failed":0,"duration_ms":39,"success_list":[{"form_sn":"life-insurance-3-d8fde07d","product_name":"长宁終身壽險計劃3","file":"计划书模版4.docx"}],"failed_list":[],"total":1},"change_summary":{"ok":true,"dry_run":false,"updated_count":1,"form_sn_list":["life-insurance-3-d8fde07d"],"conflicts":[],"reason":null}}
......
......@@ -15,6 +15,12 @@
*
* # 查看待处理文档
* npm run parse:docs -- --list
*
* # 应用审核通过的配置
* npm run parse:docs -- --apply=计划书模版4
*
* # 预览应用配置(不实际修改)
* npm run parse:docs -- --apply=计划书模版4 --dry-run
*/
import crypto from 'crypto'
import fs from 'fs'
......@@ -976,17 +982,27 @@ ${code.trim()}
## 📋 审核后操作
### 确认无误
### 方法 1:自动应用(推荐)
\`\`\`bash
# 预览变更(不实际修改)
pnpm parse:docs -- --apply=${baseFileName} --dry-run
# 确认无误后,正式应用
pnpm parse:docs -- --apply=${baseFileName}
# 说明:
# 1. 自动提取配置代码并插入到 src/config/plan-templates.js
# 2. 自动创建备份文件(docs/parsed-backup/)
# 3. 自动将审核文件移动到 docs/parse-audit/approved/
\`\`\`
### 方法 2:手动操作
\`\`\`bash
# 1. 移动到 approved 目录
mv docs/parse-audit/pending/${baseFileName}/${auditFileName} \\
docs/parse-audit/approved/
# 2. 合并到正式配置
# 手动复制或使用工具合并到 src/config/plan-templates.js
# 3. 删除待审核文件(可选)
rm docs/parse-audit/pending/${baseFileName}/${auditFileName}
# 2. 手动复制"生成配置片段"到 src/config/plan-templates.js
\`\`\`
### 需要修改
......@@ -1402,6 +1418,189 @@ function rollbackConfigFile(backupFile) {
return true
}
/**
* 从审核文件应用配置到 plan-templates.js
*
* @description 读取审核 markdown 文件,提取配置代码,插入到配置文件中
* @param {string} auditFileName - 审核文件名(不含路径,如 "计划书模版4")
* @param {Object} options - 选项
* @param {boolean} options.dry_run - 是否仅预览
* @returns {Object} 应用结果
*/
function applyAuditFile(auditFileName, options = {}) {
const PENDING_DIR = path.resolve(process.cwd(), 'docs/parse-audit/pending')
const APPROVED_DIR = path.resolve(process.cwd(), 'docs/parse-audit/approved')
// 1. 查找审核文件
let auditFile = null
let sourceDir = null
// 先在 pending 目录查找
const pendingDirs = fs.existsSync(PENDING_DIR) ? fs.readdirSync(PENDING_DIR) : []
for (const dir of pendingDirs) {
const dirPath = path.join(PENDING_DIR, dir)
if (fs.statSync(dirPath).isDirectory()) {
const files = fs.readdirSync(dirPath).filter(f => f.endsWith('.md'))
for (const file of files) {
// 匹配文件名或目录名
const normalizedName = dir.replace(/\s+/g, '').toLowerCase()
const normalizedInput = auditFileName.replace(/\s+/g, '').toLowerCase()
if (normalizedName.includes(normalizedInput) || normalizedInput.includes(normalizedName)) {
auditFile = path.join(dirPath, file)
sourceDir = PENDING_DIR
break
}
}
}
if (auditFile) break
}
// 如果 pending 没找到,在 approved 目录查找
if (!auditFile && fs.existsSync(APPROVED_DIR)) {
const approvedFiles = fs.readdirSync(APPROVED_DIR).filter(f => f.endsWith('.md'))
for (const file of approvedFiles) {
// 从文件名提取产品名(格式:YYYY-MM-DD-产品名.md)
const match = file.match(/^\d{4}-\d{2}-\d{2}-(.+)\.md$/)
if (match) {
const normalizedName = match[1].replace(/\s+/g, '').toLowerCase()
const normalizedInput = auditFileName.replace(/\s+/g, '').toLowerCase()
if (normalizedName.includes(normalizedInput) || normalizedInput.includes(normalizedName)) {
auditFile = path.join(APPROVED_DIR, file)
sourceDir = APPROVED_DIR
break
}
}
}
}
if (!auditFile) {
console.error("❌ 找不到审核文件: " + auditFileName)
console.log(" 搜索目录:")
console.log(" - docs/parse-audit/pending/")
console.log(" - docs/parse-audit/approved/")
return { ok: false, reason: 'file_not_found' }
}
console.log("\n📄 找到审核文件: " + auditFile)
// 2. 读取审核文件内容
const content = fs.readFileSync(auditFile, 'utf-8')
// 3. 提取配置代码片段
const configMatch = content.match(/## 🧩 生成配置片段\s*\n+```javascript\s*\n([\s\S]*?)```/)
if (!configMatch) {
console.error("❌ 无法从审核文件中提取配置代码")
return { ok: false, reason: 'config_not_found' }
}
const configCode = configMatch[1].trim()
console.log("\n📝 提取的配置代码:")
console.log("-".repeat(40))
console.log(configCode)
console.log("-".repeat(40))
// 4. 提取 form_sn 用于去重检查
const formSnMatch = configCode.match(/'([^']+)':\s*\{/)
const formSn = formSnMatch ? formSnMatch[1] : null
if (!formSn) {
console.error("❌ 无法从配置代码中提取 form_sn")
return { ok: false, reason: 'form_sn_not_found' }
}
console.log("\n🔑 form_sn: " + formSn)
// 5. 读取现有配置文件
const existingContent = fs.readFileSync(CONFIG_FILE, 'utf-8')
// 检查是否已存在
if (existingContent.includes(`'${formSn}':`)) {
console.error("❌ 配置文件中已存在 form_sn: " + formSn)
console.log(" 如需更新,请先手动删除旧配置")
return { ok: false, reason: 'duplicate', formSn }
}
// 6. 找到插入位置(PLAN_TEMPLATES 对象的结束位置)
// 查找最后一个产品配置的结束位置
const insertPattern = /(\n\s*'\w+[^']+':\s*\{[\s\S]*?\n\s*\}\s*,?\s*)(\n\})/
const match = existingContent.match(insertPattern)
if (!match) {
console.error("❌ 无法定位插入位置")
return { ok: false, reason: 'insert_not_found' }
}
// 7. 构建新配置(确保有逗号)
let newConfigEntry = configCode
// 确保配置以逗号结尾
if (!newConfigEntry.trimEnd().endsWith(',')) {
newConfigEntry = newConfigEntry.trimEnd() + ','
}
// 8. 插入配置
const insertPosition = match.index + match[1].length
const updatedContent =
existingContent.slice(0, insertPosition) +
'\n\n' +
newConfigEntry +
existingContent.slice(insertPosition)
if (options.dry_run) {
console.log("\n🧪 dry-run 模式,变更预览:")
console.log("-".repeat(40))
console.log("将插入以下配置:")
console.log(newConfigEntry)
console.log("-".repeat(40))
return { ok: true, dry_run: true, formSn }
}
// 9. 备份并写入
let backupFile = null
if (fs.existsSync(CONFIG_FILE)) {
ensureDir(BACKUP_DIR)
backupFile = path.join(BACKUP_DIR, `plan-templates.backup.${Date.now()}.js`)
fs.copyFileSync(CONFIG_FILE, backupFile)
console.log("\n💾 已备份到: " + backupFile)
}
writeFile(CONFIG_FILE, updatedContent)
console.log("\n✅ 配置已更新: " + CONFIG_FILE)
writeBackupLog({
action: 'apply_audit',
backup_file: backupFile,
target_file: CONFIG_FILE,
audit_file: auditFile,
form_sn: formSn,
at: new Date().toISOString()
})
// 10. 移动审核文件到 approved 目录(如果是从 pending 来的)
if (sourceDir === PENDING_DIR) {
ensureDir(APPROVED_DIR)
const fileName = path.basename(auditFile)
const approvedPath = path.join(APPROVED_DIR, fileName)
// 检查目标是否已存在
if (fs.existsSync(approvedPath)) {
console.log("⚠️ approved 目录已存在同名文件,跳过移动")
} else {
fs.renameSync(auditFile, approvedPath)
console.log("📁 审核文件已移动到: " + approvedPath)
// 删除空的 pending 子目录
const pendingSubDir = path.dirname(auditFile)
const remainingFiles = fs.readdirSync(pendingSubDir).filter(f => !f.startsWith('.'))
if (remainingFiles.length === 0) {
fs.rmdirSync(pendingSubDir)
console.log("🗑️ 已删除空目录: " + pendingSubDir)
}
}
}
return { ok: true, formSn, backupFile }
}
function updateConfigFile(newConfigs, options = {}) {
console.log("\n" + "=".repeat(60))
console.log("📝 更新配置文件: " + CONFIG_FILE)
......@@ -1563,9 +1762,16 @@ async function main() {
const listMode = args.includes('--list')
const fileMode = args.find(arg => arg.startsWith('--file='))
const writeMode = args.includes('--write-config')
const dryRunMode = args.includes('--dry-run') || !writeMode
const rollbackMode = args.find(arg => arg.startsWith('--rollback='))
const statusMode = args.includes('--status')
const applyMode = args.find(arg => arg.startsWith('--apply='))
// dry-run 逻辑:
// 1. 如果显式指定 --dry-run,则 dry-run
// 2. 如果是 apply 模式,默认不 dry-run(除非显式指定)
// 3. 如果是解析模式,默认 dry-run(除非显式指定 --write-config)
const explicitDryRun = args.includes('--dry-run')
const dryRunMode = applyMode ? explicitDryRun : (!writeMode && !explicitDryRun || explicitDryRun)
// 检查解析器选择
const parserModeArg = args.find(arg => arg.startsWith('--parser='))
......@@ -1586,6 +1792,11 @@ async function main() {
if (rollbackMode) {
const backupFile = rollbackMode.split('=')[1]
rollbackConfigFile(backupFile)
} else if (applyMode) {
// 从审核文件应用配置
const auditFileName = applyMode.split('=')[1]
const applyOptions = { dry_run: dryRunMode }
applyAuditFile(auditFileName, applyOptions)
} else if (listMode) {
// 列出模式
const docs = getDocsToParse()
......
......@@ -16,6 +16,7 @@
* - GC宏摯家傳承保險計劃- 性別, 年齡, 出生年月日
* - FA 宏浚傳承保障計劃
* - LV2 赤霞珠終身壽險計劃2基本人壽保障選項
* - LV3 长宁終身壽險計劃3
*/
const PRODUCT_TITLE_PATTERNS = [
// 产品代码 + 产品名称 + 可选后缀
......@@ -31,14 +32,17 @@ const PRODUCT_TITLE_PATTERNS = [
/^([^\n]{2,30}?(?:計劃|计划|保障|保险|壽險|壽险)[^\n]*)/gm,
// 产品代码开头的行
/^([A-Z]{2,4}\d?)\s*[-:]\s*([^\n]+)/gm
/^([A-Z]{2,4}\d?)\s*[-:]\s*([^\n]+)/gm,
// 新增:产品代码 + 产品名称 + 数字后缀(如 "LV3 长宁終身壽險計劃3")
/^([A-Z]{2,3}\d?)\s+([^\n]{2,25}?(?:計劃|计划|壽險|壽险)\d?)/gm
]
/**
* 产品代码前缀列表(用于优先匹配)
*/
const PRODUCT_CODE_PREFIXES = [
'GS', 'GC', 'FA', 'LV2', 'LV', 'CR', 'HR', 'PR', 'SR',
'GS', 'GC', 'FA', 'LV2', 'LV3', 'LV', 'CR', 'HR', 'PR', 'SR',
'TR', 'UR', 'WR', 'XR', 'YR', 'ZR'
]
......@@ -62,10 +66,11 @@ export function detectProductCount(content) {
export function findProductTitles(content) {
const products = []
const seenCodes = new Set()
const seenNames = new Set()
// 策略1: 优先匹配产品代码前缀
for (const prefix of PRODUCT_CODE_PREFIXES) {
// 匹配 "GS宏摯傳承保障計劃" 或 "GS 宏摯傳承保障計劃"
// 匹配 "GS宏摯傳承保障計劃" 或 "GS 宏摯傳承保障計劃" 或 "LV3 长宁終身壽險計劃3"
const regex = new RegExp(
`^(${prefix}\\d?)\\s*([\\u4e00-\\u9fa5]+(?:計劃|计划|保障|保险|壽險|壽险)[^\\n]*)`,
'gm'
......@@ -76,9 +81,11 @@ export function findProductTitles(content) {
const code = match[1]
const name = match[2].trim()
// 去重
if (seenCodes.has(code)) continue
// 去重(基于代码或名称)
const nameKey = name.replace(/\s+/g, '').toLowerCase()
if (seenCodes.has(code) || seenNames.has(nameKey)) continue
seenCodes.add(code)
seenNames.add(nameKey)
products.push({
index: match.index,
......@@ -99,15 +106,52 @@ export function findProductTitles(content) {
const fullTitle = match[0].trim()
if (fullTitle.length < 5) continue // 过滤太短的匹配
const code = match[1] || null
const name = match[2] || fullTitle
// 去重
const nameKey = name.replace(/\s+/g, '').toLowerCase()
if (seenNames.has(nameKey)) continue
if (code) seenCodes.add(code)
seenNames.add(nameKey)
products.push({
index: match.index,
code: match[1] || null,
name: match[2] || fullTitle,
code,
name,
fullTitle
})
}
}
// 策略3: 新增 - 识别包含"计划"但不包含产品代码的行(纯计划书名称)
// 适用于标题如 "宏挚传承保障计划" 或 "长宁终身寿险计划3"
if (products.length === 0) {
const planNameRegex = /^([^\n]{2,30}?(?:計劃|计划)[^\n]*)/gm
let match
while ((match = planNameRegex.exec(content)) !== null) {
const fullTitle = match[1].trim()
// 排除太短或包含其他关键词的行
if (fullTitle.length < 5 || fullTitle.includes('選項') || fullTitle.includes('选项')) continue
// 检查是否是产品名称(通常包含"保障"、"保险"、"寿险"等关键词)
if (/(?:保障|保险|壽險|壽险|传承|家传)/.test(fullTitle)) {
const nameKey = fullTitle.replace(/\s+/g, '').toLowerCase()
if (!seenNames.has(nameKey)) {
seenNames.add(nameKey)
products.push({
index: match.index,
code: null,
name: fullTitle.split(/[-—::]/)[0].trim(), // 移除后缀说明
fullTitle
})
}
}
}
}
// 按出现位置排序
products.sort((a, b) => a.index - b.index)
......
......@@ -8,6 +8,107 @@
*/
/**
* 保险缴费年期匹配模式
*
* @description 用于识别各种格式的缴费年期选项
* @constant {Object}
*/
const PAYMENT_PERIOD_PATTERNS = {
// 一次性缴费关键词
singlePayment: ['整付', '趸交', '躉繳', 'lump sum', 'single'],
// 数字+年格式(3年、5年、10年等)
yearlyPattern: /^(\d+)\s*年$/,
// 至X岁格式
untilAgePattern: /^至?\s*(\d+)\s*岁$/,
// 列表项格式(- 3年、• 5年等)
listItemPattern: /^[-•·]\s*(.+)$/
}
/**
* 检查是否为有效的缴费年期选项
*
* @param {string} text - 待检查文本
* @returns {boolean} 是否为有效缴费年期
*/
const isValidPaymentPeriod = (text) => {
const trimmed = text.trim()
// 排除无效关键字
if (trimmed.includes('投保') || trimmed.includes('年龄') || trimmed.includes('年齡')) {
return false
}
// 1. 匹配 "X年" 格式
if (PAYMENT_PERIOD_PATTERNS.yearlyPattern.test(trimmed)) {
return true
}
// 2. 匹配 "至X岁" 格式
if (PAYMENT_PERIOD_PATTERNS.untilAgePattern.test(trimmed)) {
return true
}
// 3. 匹配一次性缴费关键词
if (PAYMENT_PERIOD_PATTERNS.singlePayment.some(kw =>
trimmed.toLowerCase() === kw.toLowerCase()
)) {
return true
}
// 4. 匹配列表项格式(提取内容后递归检查)
const listItemMatch = trimmed.match(PAYMENT_PERIOD_PATTERNS.listItemPattern)
if (listItemMatch) {
return isValidPaymentPeriod(listItemMatch[1])
}
// 5. 通用匹配:包含"年"或"岁"或"交"的短文本(2-10字符)
if (trimmed.length >= 2 && trimmed.length <= 10) {
if (/年|岁|交|付/.test(trimmed)) {
return true
}
}
return false
}
/**
* 标准化缴费年期选项
*
* @param {string} text - 原始文本
* @returns {string} 标准化后的文本
*/
const normalizePaymentPeriod = (text) => {
const trimmed = text.trim()
// 标准化一次性缴费
if (PAYMENT_PERIOD_PATTERNS.singlePayment.some(kw =>
trimmed.toLowerCase() === kw.toLowerCase()
)) {
return '整付'
}
// 标准化 "X年" 格式
const yearlyMatch = trimmed.match(PAYMENT_PERIOD_PATTERNS.yearlyPattern)
if (yearlyMatch) {
return `${yearlyMatch[1]}年`
}
// 标准化 "至X岁" 格式
const ageMatch = trimmed.match(PAYMENT_PERIOD_PATTERNS.untilAgePattern)
if (ageMatch) {
return `至${ageMatch[1]}岁`
}
// 处理列表项格式
const listItemMatch = trimmed.match(PAYMENT_PERIOD_PATTERNS.listItemPattern)
if (listItemMatch) {
return normalizePaymentPeriod(listItemMatch[1])
}
return trimmed
}
/**
* 字段提取规则配置
*
* @description 定义每个字段的匹配规则、优先级和默认值
......@@ -20,13 +121,30 @@ const FIELD_RULES = {
/产品名称[::]\s*([^\n]+)/,
/计划书名称[::]\s*([^\n]+)/,
/Product\s+Name[::]\s*([^\n]+)/i,
/^#\s+(.+)$/m // Markdown 标题
/^#\s+(.+)$/m, // Markdown 标题
// 新增:识别包含"计划"的标题行(如 "LV3 长宁終身壽險計劃3")
// 格式:产品代码 + 中文产品名(包含"計劃/计划")
/^[A-Z]{2,4}\d?\s*[-::]?\s*([^\n]*(?:計劃|计划)[^\n]*)/m,
// 纯产品名称(包含"計劃/计划")- 通常为计划书名称
/^([^\n]{2,30}?(?:計劃|计划)[^\n]*)/m
],
fallback: null, // 必填,无默认值
required: true
required: true,
postProcess: (value) => {
// 处理正则匹配结果(数组)
if (Array.isArray(value) && value.length > 1) {
let name = value[1] || value[0] || ''
// 移除产品代码前缀
name = name.replace(/^[A-Z]{2,4}\d?\s*[-::]?\s*/, '')
// 移除后缀说明(如 "- 性別, 年齡, 出生年月日")
name = name.split(/[-—::]/)[0].trim()
return name || null
}
return value
}
},
// 产品类型
// 产品类型(保险类别)
product_type: {
priority: 2,
patterns: [
......@@ -34,9 +152,21 @@ const FIELD_RULES = {
{
type: 'content_match',
rules: [
{ keywords: ['储蓄', 'saving', '传承', '家传', '红利', '提取'], value: 'savings' },
// 储蓄型产品(新增"储蓄产品"关键字)
{ keywords: ['储蓄产品', '储蓄型', '储蓄', 'saving', '传承', '家传', '红利', '提取'], value: 'savings' },
// 重疾型产品
{ keywords: ['重疾', 'critical', '守护', '严重疾病'], value: 'critical-illness' },
{ keywords: ['人寿', 'life', '创富', '身故保障'], value: 'life-insurance' }
// 人寿型产品
{ keywords: ['人寿', 'life', '创富', '身故保障', '壽險', '壽险'], value: 'life-insurance' }
]
},
// 新增:从标题行推断(如 "LV3 长宁終身壽險計劃3" 中的"壽險")
{
type: 'title_match',
rules: [
{ pattern: /終身壽險|終身寿险|壽險計劃|寿险计划/, value: 'life-insurance' },
{ pattern: /儲蓄計劃|储蓄计划|儲蓄產品|储蓄产品/, value: 'savings' },
{ pattern: /重疾|嚴重疾病/, value: 'critical-illness' }
]
}
],
......@@ -70,25 +200,12 @@ const FIELD_RULES = {
priority: 4,
patterns: [
// 匹配 "年繳保費繳費年期" 或 "缴费年期" 后面的列表
// 策略:匹配到包含 "年" 或 "整付" 的所有行,直到遇到其他关键字
// 策略:使用通用匹配函数识别各种格式的缴费年期选项
{
type: 'smart_list_extract',
startPattern: /(?:年繳保費)?繳費年期[::\s]*\n/,
endKeywords: ['提取', '保險期間', '保险期间', '投保年龄', '投保年齡', '選是', '選項', 'GC宏', 'FA宏', 'LV2'],
itemFilter: (line) => {
const trimmed = line.trim()
// 排除包含"投保年龄"等关键字的行
if (trimmed.includes('投保') || trimmed.includes('年龄') || trimmed.includes('年齡')) {
return false
}
// 精确匹配 "整付" 或 "X年" 格式
return trimmed && (
/^\d+\s*年$/.test(trimmed) ||
trimmed === '整付' ||
/^\d+年$/.test(trimmed) ||
/^[-•·]\s*\d+\s*年$/.test(trimmed) // 支持列表格式 "- 3年"
)
}
itemFilter: (line) => isValidPaymentPeriod(line)
}
],
fallback: ['整付', '3年', '5年'],
......@@ -96,24 +213,17 @@ const FIELD_RULES = {
postProcess: (values) => {
// 过滤并标准化
const normalized = values
.map(v => v.trim())
// 排除包含"投保"等无效关键字
.filter(v => v && !v.includes('投保') && !v.includes('年龄') && !v.includes('年齡'))
.filter(v => v.includes('年') || v.includes('整付'))
.map(v => {
// 提取数字+年格式
const match = v.match(/(\d+)\s*年|整付/i)
if (match) {
return match[0].includes('整付') ? '整付' : `${match[1]}年`
}
return v
})
.map(v => normalizePaymentPeriod(v))
.filter(v => v && isValidPaymentPeriod(v))
// 去重、排序
// 去重、排序(整付放最前,其他按数字排序)
return [...new Set(normalized)].sort((a, b) => {
if (a === '整付') return -1
if (b === '整付') return 1
return parseInt(a) - parseInt(b)
// 提取数字进行排序
const numA = parseInt(a.match(/\d+/)?.[0] || '999')
const numB = parseInt(b.match(/\d+/)?.[0] || '999')
return numA - numB
})
}
},
......@@ -252,6 +362,11 @@ function extractField(content, fieldName) {
patternDesc = `content_match(${pattern.rules.length} rules)`
break
case 'title_match':
match = matchByTitle(content, pattern.rules)
patternDesc = `title_match(${pattern.rules.length} rules)`
break
case 'count_match':
match = matchByCount(content, pattern.rules)
patternDesc = `count_match(${pattern.rules.length} rules)`
......@@ -276,6 +391,11 @@ function extractField(content, fieldName) {
match = extractRange(content, pattern.pattern)
patternDesc = `range_extract`
break
case 'options_extract':
match = extractOptionsFields(content, pattern.keyword)
patternDesc = `options_extract(${pattern.keyword})`
break
}
} else if (pattern instanceof RegExp) {
// 正则表达式匹配
......@@ -331,6 +451,120 @@ function matchByContent(content, rules) {
}
/**
* 通过标题模式匹配
*
* @description 从文档标题或产品名称行中匹配产品类型
* @param {string} content - 文档内容
* @param {Array} rules - 匹配规则数组
* @returns {string|null} 匹配的值
*/
function matchByTitle(content, rules) {
// 提取可能的产品标题行(通常在文档开头)
const lines = content.split('\n').slice(0, 20) // 只检查前20行
const titleLines = lines.filter(line => {
const trimmed = line.trim()
// 标题行通常包含产品代码、产品名称等
return trimmed.length > 5 && trimmed.length < 100 &&
(/[A-Z]{2,4}\d?/.test(trimmed) ||
/計劃|计划|保障|保险|壽險|壽险/.test(trimmed))
})
// 合并标题行进行匹配
const titleText = titleLines.join('\n')
for (const rule of rules) {
if (rule.pattern && rule.pattern.test(titleText)) {
return rule.value
}
}
return null
}
/**
* 从"选项"段落提取字段
*
* @description 识别 "XXX選項:" 或 "XXX选项:" 格式的段落,提取其中的字段列表
* 示例:
* - "基本人壽保障選項:" 之后的性别、年龄等字段
* - "提取選項:" 之后的提取方式
* @param {string} content - 文档内容
* @param {string} optionKeyword - 选项关键词(如 "基本人壽保障選項")
* @returns {Array<{name: string, description: string}>|null} 提取的字段列表
*/
function extractOptionsFields(content, optionKeyword) {
// 匹配 "XXX選項:" 或 "XXX选项:" 格式
const pattern = new RegExp(`${optionKeyword}[::\\s]*\\n([\\s\\S]*?)(?=\\n\\n|\\n[A-Z]|\\n\\d+\\.\\s|$)`, 'gm')
const match = content.match(pattern)
if (!match) return null
const optionContent = match[0]
const fields = []
// 按行分割,提取字段名和描述
const lines = optionContent.split('\n').slice(1) // 跳过标题行
for (const line of lines) {
const trimmed = line.trim()
if (!trimmed) continue
// 常见字段格式:
// 1. "性別" - 纯字段名
// 2. "年齡 (1-75岁)" - 字段名 + 范围说明
// 3. "- 出生年月日" - 列表格式
// 4. "提取金额: 指定金额" - 字段名 + 描述
// 提取字段名(去除列表符号和说明)
const fieldMatch = trimmed.match(/^(?:[-•·]\s*)?([^::((]+)/)
if (fieldMatch) {
const fieldName = fieldMatch[1].trim()
if (fieldName && fieldName.length > 0 && fieldName.length < 20) {
fields.push({
name: fieldName,
description: trimmed // 保留完整描述
})
}
}
}
return fields.length > 0 ? fields : null
}
/**
* 查找所有"选项"段落
*
* @description 扫描文档中所有包含"選項"或"选项"的段落标题
* @param {string} content - 文档内容
* @returns {Array<{keyword: string, startIndex: number, fields: Array}>} 选项段落列表
*/
function findAllOptionSections(content) {
const sections = []
// 匹配所有 "XXX選項:" 或 "XXX选项:" 格式
const pattern = /([^\n]*(?:選項|选项|選是)[::]?)/gm
let match
while ((match = pattern.exec(content)) !== null) {
const keyword = match[1].trim()
const startIndex = match.index
// 提取该选项下的字段
const fields = extractOptionsFields(content, keyword.replace(/[::]/g, ''))
if (fields && fields.length > 0) {
sections.push({
keyword,
startIndex,
fields
})
}
}
return sections
}
/**
* 通过统计匹配内容
*/
function matchByCount(content, rules) {
......
......@@ -368,6 +368,24 @@ export const PLAN_TEMPLATES = {
submit_mapping: savingsSubmitMapping
}
},
/**
* 长宁終身壽險計劃3
* @added 2026-02-15T06:30:03.691Z
* @source docs/to-parse/计划书模版4.docx
*/
'life-insurance-3-d8fde07d': {
name: '长宁終身壽險計劃3',
component: 'LifeInsuranceTemplate',
config: {
currency: 'USD',
payment_periods: ["5年","12年","15年","20年"],
age_range: { min: 0, max: 75 },
insurance_period: '终身',
form_schema: protectionFormSchema,
submit_mapping: baseSubmitMapping
}
}
}
/**
......
......@@ -415,9 +415,11 @@ export async function mockProductListAPI(params) {
const list = []
const startIndex = page * limit
// 🔧 测试商品:第一页第一位固定为储蓄产品(form_sn:savings-product-30b41aae)
// 🔧 测试商品:第一页前两位固定为测试产品
if (page === 0) {
const testCategory = PRODUCT_CATEGORIES.find(c => parseInt(c.id) === 1)
// 测试商品1: 储蓄产品
const testProduct1 = {
id: 'savings-2-148b3acd',
product_name: '测试计划书-智享未来2(form_sn:savings-2-148b3acd)',
......@@ -432,19 +434,38 @@ export async function mockProductListAPI(params) {
_test_note: 'form_sn:savings-2-148b3acd'
}
// 检查分类和关键词过滤
let shouldInclude = true
if (cid && !testProduct1.categories.some(c => parseInt(c.id) === parseInt(cid))) {
shouldInclude = false
}
if (keyword && !testProduct1.product_name.includes(keyword)) {
shouldInclude = false
// 测试商品2: 人寿保险产品
const testProduct2 = {
id: 'life-insurance-3-d8fde07d',
product_name: '测试计划书-人生无忧3(form_sn:life-insurance-3-d8fde07d)',
cover_image: 'https://picsum.photos/seed/life-insurance-3-d8fde07d/400/300',
recommend: 'hot',
form_sn: 'life-insurance-3-d8fde07d', // ✅ 关键字段:对应真实 API 的 form_sn
created_time: new Date().toISOString(),
categories: [testCategory], // ✅ 符合真实 API 结构:categories 是数组
tags: [{ id: '1', name: '热销', bg_color: '#FEE2E2', text_color: '#DC2626' }],
// 测试标识(不影响业务逻辑)
_test: true,
_test_note: 'form_sn:life-insurance-3-d8fde07d'
}
if (shouldInclude) {
list.push(testProduct1)
console.log('[Mock] listAPI - 测试商品已置顶: form_sn=savings-2-148b3acd')
}
// 检查分类和关键词过滤,依次添加测试商品
const testProducts = [testProduct1, testProduct2]
testProducts.forEach((testProduct, index) => {
let shouldInclude = true
if (cid && !testProduct.categories.some(c => parseInt(c.id) === parseInt(cid))) {
shouldInclude = false
}
if (keyword && !testProduct.product_name.includes(keyword)) {
shouldInclude = false
}
if (shouldInclude) {
list.push(testProduct)
console.log(`[Mock] listAPI - 测试商品${index + 1}已置顶: form_sn=${testProduct.form_sn}`)
}
})
}
for (let i = 0; i < limit; i++) {
......