hookehuyr

docs(plan): 添加计划书配置架构文档和后端迁移指南

- 新增计划书配置架构详解文档
- 新增后端迁移指南(含数据流图、API设计、实施步骤)
- 新增配置 Schema 参考文档
- 新增 Draw.io 架构可视化图

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
<mxfile host="app.diagrams.net" modified="2026-02-25T00:00:00.000Z" agent="5.0" version="24.0.0">
<diagram id="plan-migration" name="计划书后端迁移架构">
<mxGraphModel dx="1400" dy="900" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1400" pageHeight="1200" math="0" shadow="0">
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="title" value="计划书后端迁移架构图" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=24;fontStyle=1" vertex="1" parent="1">
<mxGeometry x="450" y="20" width="500" height="50" as="geometry"/>
</mxCell>
<mxCell id="frontend-title" value="前端架构(当前)" style="swimlane;fontSize=16;fontStyle=1;childLayout=stackLayout;horizontal=1;startSize=40;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;strokeWidth=2;" vertex="1" parent="1">
<mxGeometry x="60" y="100" width="560" height="680" as="geometry"/>
</mxCell>
<mxCell id="config-box" value="配置层 Config" style="swimlane;fontSize=14;fontStyle=1;childLayout=stackLayout;horizontal=1;startSize=40;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;strokeWidth=2;" vertex="1" parent="frontend-title">
<mxGeometry y="40" width="560" height="180" as="geometry"/>
</mxCell>
<mxCell id="plan-templates" value="plan-templates.js&#xa;产品映射: form_sn → TemplateConfig&#xa;基础 Schema、字段映射" style="align=left;strokeColor=none;fillColor=#d5e8d4;strokeColor=#82b366;spacingLeft=8;spacingRight=8;spacingTop=8;spacingBottom=8;whiteSpace=wrap;html=1;verticalAlign=top;fontSize=12;rounded=1;" vertex="1" parent="config-box">
<mxGeometry y="40" width="560" height="50" as="geometry"/>
</mxCell>
<mxCell id="plan-fields" value="plan-fields.js&#xa;字段类型定义、验证规则、字段分组" style="align=left;strokeColor=none;fillColor=#d5e8d4;strokeColor=#82b366;spacingLeft=8;spacingRight=8;spacingTop=8;spacingBottom=8;whiteSpace=wrap;html=1;verticalAlign=top;fontSize=12;rounded=1;" vertex="1" parent="config-box">
<mxGeometry y="100" width="560" height="50" as="geometry"/>
</mxCell>
<mxCell id="container-box" value="容器层 Container" style="swimlane;fontSize=14;fontStyle=1;childLayout=stackLayout;horizontal=1;startSize=40;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;strokeWidth=2;" vertex="1" parent="frontend-title">
<mxGeometry y="220" width="560" height="140" as="geometry"/>
</mxCell>
<mxCell id="plan-form-container" value="PlanFormContainer.vue&#xa;1. 根据 form_sn 获取配置&#xa;2. 合并 product.plan_config&#xa;3. 动态加载模板组件" style="align=left;strokeColor=none;fillColor=#e1d5e7;strokeColor=#9673a6;spacingLeft=8;spacingRight=8;spacingTop=8;spacingBottom=8;whiteSpace=wrap;html=1;verticalAlign=top;fontSize=12;rounded=1;" vertex="1" parent="container-box">
<mxGeometry y="40" width="560" height="100" as="geometry"/>
</mxCell>
<mxCell id="template-box" value="模板层 Template" style="swimlane;fontSize=14;fontStyle=1;childLayout=stackLayout;horizontal=1;startSize=40;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;strokeWidth=2;" vertex="1" parent="frontend-title">
<mxGeometry y="360" width="560" height="180" as="geometry"/>
</mxCell>
<mxCell id="life-template" value="LifeInsuranceTemplate&#xa;人寿保险模板" style="align=left;strokeColor=none;fillColor=#f8cecc;strokeColor=#b85450;spacingLeft=8;spacingRight=8;spacingTop=8;spacingBottom=8;whiteSpace=wrap;html=1;verticalAlign=top;fontSize=12;rounded=1;" vertex="1" parent="template-box">
<mxGeometry y="40" width="170" height="50" as="geometry"/>
</mxCell>
<mxCell id="critical-template" value="CriticalIllnessTemplate&#xa;重疾保险模板" style="align=left;strokeColor=none;fillColor=#f8cecc;strokeColor=#b85450;spacingLeft=8;spacingRight=8;spacingTop=8;spacingBottom=8;whiteSpace=wrap;html=1;verticalAlign=top;fontSize=12;rounded=1;" vertex="1" parent="template-box">
<mxGeometry y="100" width="170" height="50" as="geometry"/>
</mxCell>
<mxCell id="savings-template" value="SavingsTemplate&#xa;储蓄型保险模板" style="align=left;strokeColor=none;fillColor=#f8cecc;strokeColor=#b85450;spacingLeft=8;spacingRight=8;spacingTop=8;spacingBottom=8;whiteSpace=wrap;html=1;verticalAlign=top;fontSize=12;rounded=1;" vertex="1" parent="template-box">
<mxGeometry y="160" width="170" height="20" as="geometry"/>
</mxCell>
<mxCell id="fields-box" value="字段组件层 Fields" style="swimlane;fontSize=14;fontStyle=1;childLayout=stackLayout;horizontal=1;startSize=40;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;strokeWidth=2;" vertex="1" parent="frontend-title">
<mxGeometry y="540" width="560" height="100" as="geometry"/>
</mxCell>
<mxCell id="field-components" value="NameInput | AmountKeyboard | DatePicker | Radio | Select | AgePicker | PaymentPeriodRadio" style="align=left;strokeColor=none;fillColor=#ffe6cc;strokeColor=#d79b00;spacingLeft=8;spacingRight=8;spacingTop=8;spacingBottom=8;whiteSpace=wrap;html=1;verticalAlign=top;fontSize=12;rounded=1;" vertex="1" parent="fields-box">
<mxGeometry y="40" width="560" height="60" as="geometry"/>
</mxCell>
<mxCell id="backend-title" value="后端架构(迁移后)" style="swimlane;fontSize=16;fontStyle=1;childLayout=stackLayout;horizontal=1;startSize=40;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;strokeWidth=2;" vertex="1" parent="1">
<mxGeometry x="680" y="100" width="560" height="680" as="geometry"/>
</mxCell>
<mxCell id="database-box" value="数据存储层" style="swimlane;fontSize=14;fontStyle=1;childLayout=stackLayout;horizontal=1;startSize=40;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;strokeWidth=2;" vertex="1" parent="backend-title">
<mxGeometry y="40" width="560" height="140" as="geometry"/>
</mxCell>
<mxCell id="product-table" value="product_configs 表&#xa;&#xa;form_sn (PK) | name | component&#xa;config (JSON): currency, form_schema, submit_mapping" style="align=left;strokeColor=none;fillColor=#ffffff;strokeColor=#82b366;spacingLeft=8;spacingRight=8;spacingTop=8;spacingBottom=8;whiteSpace=wrap;html=1;verticalAlign=top;fontSize=11;fontFamily=Courier New;rounded=1;" vertex="1" parent="database-box">
<mxGeometry y="40" width="560" height="100" as="geometry"/>
</mxCell>
<mxCell id="api-box" value="API 层" style="swimlane;fontSize=14;fontStyle=1;childLayout=stackLayout;horizontal=1;startSize=40;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;strokeWidth=2;" vertex="1" parent="backend-title">
<mxGeometry y="180" width="560" height="240" as="geometry"/>
</mxCell>
<mxCell id="api-get" value="GET /api/plan/config/:form_sn" style="align=left;strokeColor=none;fillColor=#ffffff;strokeColor=#82b366;spacingLeft=8;spacingRight=8;spacingTop=8;spacingBottom=8;whiteSpace=wrap;html=1;verticalAlign=top;fontSize=12;rounded=1;" vertex="1" parent="api-box">
<mxGeometry y="40" width="560" height="40" as="geometry"/>
</mxCell>
<mxCell id="api-batch" value="GET /api/plan/config/batch" style="align=left;strokeColor=none;fillColor=#ffffff;strokeColor=#82b366;spacingLeft=8;spacingRight=8;spacingTop=8;spacingBottom=8;whiteSpace=wrap;html=1;verticalAlign=top;fontSize=12;rounded=1;" vertex="1" parent="api-box">
<mxGeometry y="80" width="560" height="40" as="geometry"/>
</mxCell>
<mxCell id="api-add" value="POST /srv/?a=proposal&amp;t=add" style="align=left;strokeColor=none;fillColor=#ffffff;strokeColor=#82b366;spacingLeft=8;spacingRight=8;spacingTop=8;spacingBottom=8;whiteSpace=wrap;html=1;verticalAlign=top;fontSize=12;rounded=1;" vertex="1" parent="api-box">
<mxGeometry y="120" width="560" height="40" as="geometry"/>
</mxCell>
<mxCell id="api-list" value="GET /srv/?a=proposal&amp;t=list" style="align=left;strokeColor=none;fillColor=#ffffff;strokeColor=#82b366;spacingLeft=8;spacingRight=8;spacingTop=8;spacingBottom=8;whiteSpace=wrap;html=1;verticalAlign=top;fontSize=12;rounded=1;" vertex="1" parent="api-box">
<mxGeometry y="160" width="560" height="40" as="geometry"/>
</mxCell>
<mxCell id="admin-box" value="配置管理后台" style="swimlane;fontSize=14;fontStyle=1;childLayout=stackLayout;horizontal=1;startSize=40;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;strokeWidth=2;" vertex="1" parent="backend-title">
<mxGeometry y="420" width="560" height="180" as="geometry"/>
</mxCell>
<mxCell id="admin-crud" value="配置 CRUD 操作:创建/编辑/删除/预览" style="align=left;strokeColor=none;fillColor=#ffffff;strokeColor=#82b366;spacingLeft=8;spacingRight=8;spacingTop=8;spacingBottom=8;whiteSpace=wrap;html=1;verticalAlign=top;fontSize=12;rounded=1;" vertex="1" parent="admin-box">
<mxGeometry y="40" width="560" height="40" as="geometry"/>
</mxCell>
<mxCell id="admin-parser" value="文档解析器集成:AI 智能提取配置" style="align=left;strokeColor=none;fillColor=#ffffff;strokeColor=#82b366;spacingLeft=8;spacingRight=8;spacingTop=8;spacingBottom=8;whiteSpace=wrap;html=1;verticalAlign=top;fontSize=12;rounded=1;" vertex="1" parent="admin-box">
<mxGeometry y="80" width="560" height="40" as="geometry"/>
</mxCell>
<mxCell id="admin-validator" value="配置验证器:JSON Schema 验证" style="align=left;strokeColor=none;fillColor=#ffffff;strokeColor=#82b366;spacingLeft=8;spacingRight=8;spacingTop=8;spacingBottom=8;whiteSpace=wrap;html=1;verticalAlign=top;fontSize=12;rounded=1;" vertex="1" parent="admin-box">
<mxGeometry y="120" width="560" height="40" as="geometry"/>
</mxCell>
<mxCell id="arrow1" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=3;strokeColor=#666666;" edge="1" parent="1" source="plan-templates" target="plan-form-container">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="arrow1-label" value="getTemplateConfig" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=11;fontColor=#666666;fontStyle=1" vertex="1" connectable="0" parent="arrow1">
<mxGeometry x="-0.1" y="1" relative="1" as="geometry">
<mxPoint y="-5" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="arrow2" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=3;strokeColor=#666666;" edge="1" parent="1" source="plan-form-container" target="life-template">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="arrow2-label" value="动态加载模板" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=11;fontColor=#666666;fontStyle=1" vertex="1" connectable="0" parent="arrow2">
<mxGeometry x="-0.1" y="1" relative="1" as="geometry">
<mxPoint y="-5" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="arrow3" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=3;strokeColor=#666666;" edge="1" parent="1" source="life-template" target="field-components">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="arrow3-label" value="渲染字段组件" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=11;fontColor=#666666;fontStyle=1" vertex="1" connectable="0" parent="arrow3">
<mxGeometry x="-0.1" y="1" relative="1" as="geometry">
<mxPoint y="-5" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="arrow4" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=4;strokeColor=#d79b00;dashed=1;dashPattern=8 8;" edge="1" parent="1" source="plan-form-container" target="api-get">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="620" y="340"/>
<mxPoint x="620" y="270"/>
</Array>
</mxGeometry>
</mxCell>
<mxCell id="arrow4-label" value="GET /api/plan/config (迁移后)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=11;fontColor=#d79b00;fontStyle=1" vertex="1" connectable="0" parent="arrow4">
<mxGeometry x="-0.2" y="1" relative="1" as="geometry">
<mxPoint y="-10" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="arrow5" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=4;strokeColor=#82b366;" edge="1" parent="1" source="plan-form-container" target="api-add">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="620" y="380"/>
<mxPoint x="620" y="320"/>
</Array>
</mxGeometry>
</mxCell>
<mxCell id="arrow5-label" value="提交订单" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=11;fontColor=#82b366;fontStyle=1" vertex="1" connectable="0" parent="arrow5">
<mxGeometry x="-0.15" y="1" relative="1" as="geometry">
<mxPoint y="-5" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="arrow6" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=3;strokeColor=#666666;" edge="1" parent="1" source="api-get" target="product-table">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="arrow7" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=3;strokeColor=#666666;" edge="1" parent="1" source="admin-crud" target="product-table">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="1240" y="520"/>
</Array>
</mxGeometry>
</mxCell>
<mxCell id="solutions-box" value="迁移方案对比" style="swimlane;fontSize=16;fontStyle=1;childLayout=stackLayout;horizontal=1;startSize=40;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;strokeWidth=2;" vertex="1" parent="1">
<mxGeometry x="60" y="820" width="1180" height="200" as="geometry"/>
</mxCell>
<mxCell id="solution-a" value="方案 A:完全后端化&#xa;优势 前端代码最少 配置集中&#xa;劣势 后端复杂度高 需维护表单引擎&#xa;推荐 3星" style="align=left;strokeColor=none;fillColor=#f8cecc;strokeColor=#b85450;spacingLeft=8;spacingRight=8;spacingTop=8;spacingBottom=8;whiteSpace=wrap;html=1;verticalAlign=top;fontSize=11;rounded=1;" vertex="1" parent="solutions-box">
<mxGeometry y="40" width="360" height="60" as="geometry"/>
</mxCell>
<mxCell id="solution-b" value="方案 B:配置 API 化 推荐&#xa;优势 前端保留灵活性 后端只管理配置&#xa;劣势 前端仍需维护模板组件&#xa;推荐 5星" style="align=left;strokeColor=none;fillColor=#d5e8d4;strokeColor=#82b366;spacingLeft=8;spacingRight=8;spacingTop=8;spacingBottom=8;whiteSpace=wrap;html=1;verticalAlign=top;fontSize=11;fontStyle=1;rounded=1;" vertex="1" parent="solutions-box">
<mxGeometry y="100" width="360" height="60" as="geometry"/>
</mxCell>
<mxCell id="solution-c" value="方案 C:混合模式&#xa;优势 平衡前后端职责 渐进式迁移&#xa;劣势 架构稍复杂&#xa;推荐 4星" style="align=left;strokeColor=none;fillColor=#fff2cc;strokeColor=#d6b656;spacingLeft=8;spacingRight=8;spacingTop=8;spacingBottom=8;whiteSpace=wrap;html=1;verticalAlign=top;fontSize=11;rounded=1;" vertex="1" parent="solutions-box">
<mxGeometry y="160" width="360" height="40" as="geometry"/>
</mxCell>
<mxCell id="migration-steps" value="迁移步骤 4 个阶段&#xa;&#xa;阶段 1 后端配置存储 1-2 周 创建表 迁移配置 CRUD API&#xa;阶段 2 前端 API 集成 1 周 Store 容器 缓存&#xa;阶段 3 配置管理后台 1-2 周 可视化 预览 测试&#xa;阶段 4 文档解析集成 1 周 连接 AI 到配置" style="align=left;strokeColor=none;fillColor=#dae8fc;strokeColor=#6c8ebf;spacingLeft=8;spacingRight=8;spacingTop=8;spacingBottom=8;whiteSpace=wrap;html=1;verticalAlign=top;fontSize=11;rounded=1;" vertex="1" parent="solutions-box">
<mxGeometry x="380" y="40" width="780" height="160" as="geometry"/>
</mxCell>
<mxCell id="legend-box" value="图例" style="swimlane;fontSize=14;fontStyle=1;childLayout=stackLayout;horizontal=1;startSize=40;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;strokeWidth=2;" vertex="1" parent="1">
<mxGeometry x="60" y="1050" width="1180" height="100" as="geometry"/>
</mxCell>
<mxCell id="legend-config" value="" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;strokeWidth=1;" vertex="1" parent="legend-box">
<mxGeometry x="10" y="45" width="25" height="25" as="geometry"/>
</mxCell>
<mxCell id="legend-config-label" value="配置文件" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=12;" vertex="1" parent="legend-box">
<mxGeometry x="45" y="45" width="90" height="25" as="geometry"/>
</mxCell>
<mxCell id="legend-component" value="" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;strokeWidth=1;" vertex="1" parent="legend-box">
<mxGeometry x="150" y="45" width="25" height="25" as="geometry"/>
</mxCell>
<mxCell id="legend-component-label" value="Vue 组件" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=12;" vertex="1" parent="legend-box">
<mxGeometry x="185" y="45" width="90" height="25" as="geometry"/>
</mxCell>
<mxCell id="legend-template" value="" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;strokeWidth=1;" vertex="1" parent="legend-box">
<mxGeometry x="290" y="45" width="25" height="25" as="geometry"/>
</mxCell>
<mxCell id="legend-template-label" value="模板组件" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=12;" vertex="1" parent="legend-box">
<mxGeometry x="325" y="45" width="90" height="25" as="geometry"/>
</mxCell>
<mxCell id="legend-field" value="" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;strokeWidth=1;" vertex="1" parent="legend-box">
<mxGeometry x="430" y="45" width="25" height="25" as="geometry"/>
</mxCell>
<mxCell id="legend-field-label" value="字段组件" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=12;" vertex="1" parent="legend-box">
<mxGeometry x="465" y="45" width="90" height="25" as="geometry"/>
</mxCell>
<mxCell id="legend-api" value="" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffffff;strokeColor=#82b366;strokeWidth=1;" vertex="1" parent="legend-box">
<mxGeometry x="570" y="45" width="25" height="25" as="geometry"/>
</mxCell>
<mxCell id="legend-api-label" value="API 接口" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=12;" vertex="1" parent="legend-box">
<mxGeometry x="605" y="45" width="90" height="25" as="geometry"/>
</mxCell>
<mxCell id="legend-arrow-solid" value="" style="endArrow=classic;html=1;strokeWidth=3;strokeColor=#666666;" edge="1" parent="legend-box">
<mxGeometry width="60" height="40" relative="1" as="geometry">
<mxPoint x="730" y="55" as="sourcePoint"/>
<mxPoint x="790" y="55" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="legend-arrow-solid-label" value="数据流向" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=12;" vertex="1" parent="legend-box">
<mxGeometry x="800" y="45" width="90" height="25" as="geometry"/>
</mxCell>
<mxCell id="legend-arrow-dashed" value="" style="endArrow=classic;html=1;strokeWidth=3;strokeColor=#d79b00;dashed=1;dashPattern=8 8;" edge="1" parent="legend-box">
<mxGeometry width="60" height="40" relative="1" as="geometry">
<mxPoint x="910" y="55" as="sourcePoint"/>
<mxPoint x="970" y="55" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="legend-arrow-dashed-label" value="迁移后 API" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=12;" vertex="1" parent="legend-box">
<mxGeometry x="980" y="45" width="100" height="25" as="geometry"/>
</mxCell>
<mxCell id="legend-arrow-green" value="" style="endArrow=classic;html=1;strokeWidth=4;strokeColor=#82b366;" edge="1" parent="legend-box">
<mxGeometry width="60" height="40" relative="1" as="geometry">
<mxPoint x="1100" y="55" as="sourcePoint"/>
<mxPoint x="1160" y="55" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="legend-arrow-green-label" value="提交订单" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=12;" vertex="1" parent="legend-box">
<mxGeometry x="1170" y="45" width="90" height="25" as="geometry"/>
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>
# 计划书配置架构与后端迁移指南
> **文档目的**:详细说明计划书配置生成逻辑,为后端迁移提供完整的技术参考
**作者**:Claude Code
**创建日期**:2026-02-25
**版本**:1.0.0
---
## 目录
1. [架构概览](#架构概览)
2. [核心数据结构](#核心数据结构)
3. [配置生成流程](#配置生成流程)
4. [后端迁移方案](#后端迁移方案)
5. [API 设计建议](#api-设计建议)
6. [数据模型设计](#数据模型设计)
---
## 架构概览
### 整体架构图
```
┌─────────────────────────────────────────────────────────────────────────┐
│ 前端(当前架构) │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 配置层 (Config) │ │
│ │ │ │
│ │ ┌──────────────────────────────────────────────────────────┐ │ │
│ │ │ plan-templates.js - 产品模板配置 │ │ │
│ │ │ ├─ 产品映射: form_sn → TemplateConfig │ │ │
│ │ │ ├─ 基础 Schema: protectionFormSchema, savingsFormSchema │ │ │
│ │ │ └─ 字段映射: baseSubmitMapping, savingsSubmitMapping │ │ │
│ │ └──────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ ┌──────────────────────────────────────────────────────────┐ │ │
│ │ │ plan-fields.js - 字段定义库 │ │ │
│ │ │ ├─ 字段类型: TEXT, AMOUNT, DATE, RADIO, SELECT... │ │ │
│ │ │ ├─ 字段分组: BASIC, COVERAGE, WITHDRAWAL │ │ │
│ │ │ └─ 验证规则: required, min, max, range │ │ │
│ │ └──────────────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 容器层 (Container) │ │
│ │ │ │
│ │ ┌──────────────────────────────────────────────────────────┐ │ │
│ │ │ PlanFormContainer.vue │ │ │
│ │ │ ├─ 接收产品对象 (product.form_sn) │ │ │
│ │ │ ├─ 匹配模板配置 (getTemplateConfig) │ │ │
│ │ │ ├─ 合并后端配置 (product.plan_config) │ │ │
│ │ │ └─ 动态加载模板组件 │ │ │
│ │ └──────────────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 模板层 (Template) │ │
│ │ │ │
│ │ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────┐ │ │
│ │ │ LifeInsurance │ │ CriticalIllness │ │ Savings │ │ │
│ │ │ Template.vue │ │ Template.vue │ │ Template.vue │ │ │
│ │ │ │ │ │ │ │ │ │
│ │ │ - 基础字段 │ │ - 基础字段 │ │ - 基础字段 │ │ │
│ │ │ - 保障配置 │ │ - 保障配置 │ │ - 提取计划 │ │ │
│ │ └──────────────────┘ └──────────────────┘ └──────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 字段组件层 (Fields) │ │
│ │ │ │
│ │ NameInput │ AmountKeyboard │ DatePicker │ Radio │ AgePicker │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
│ 提交订单
┌─────────────────────────────────────────────────────────────────────────┐
│ 后端 API │
│ /srv/?a=proposal&t=add │
│ - customer_name │
│ - customer_gender │
│ - annual_premium (需要 fen→yuan 转换) │
│ - payment_years │
│ - withdrawal_option │
│ ... │
└─────────────────────────────────────────────────────────────────────────┘
```
### 数据流图
```
产品列表 API
│ { id, product_name, form_sn, plan_config? }
┌─────────────────────────────────────────────────────────────┐
│ PlanFormContainer │
│ │
│ 1. 根据 form_sn 从 plan-templates.js 获取配置 │
│ 2. 合并 product.plan_config (后端覆盖) │
│ 3. 动态加载对应 Template 组件 │
└─────────────────────────────────────────────────────────────┘
│ { config: { currency, payment_periods, form_schema, ... } }
┌─────────────────────────────────────────────────────────────┐
│ Template Component (LifeInsuranceTemplate 等) │
│ │
│ 1. 解析 form_schema.base_fields 和 withdrawal_fields │
│ 2. 根据 field.type 渲染对应的字段组件 │
│ 3. 使用 useFieldDependencies 管理字段联动 │
└─────────────────────────────────────────────────────────────┘
│ 用户填写表单
│ v-model 双向绑定
┌─────────────────────────────────────────────────────────────┐
│ 表单数据 formData │
│ { customer_name, gender, birthday, coverage, ... } │
└─────────────────────────────────────────────────────────────┘
│ 点击"生成计划书"
┌─────────────────────────────────────────────────────────────┐
│ submit() 方法 │
│ │
│ 1. 调用 template.validate() 校验表单 │
│ 2. 根据 submit_mapping 映射字段 │
│ 3. 执行值转换 (fen_to_yuan) │
│ 4. 调用 addAPI(formData) │
└─────────────────────────────────────────────────────────────┘
```
---
## 核心数据结构
### 1. TemplateConfig(模板配置)
```javascript
// 位置: src/config/plan-templates.js
const PLAN_TEMPLATES = {
'savings-gs': { // form_sn: 产品唯一标识
name: '宏挚传承保障计划', // 产品名称
component: 'SavingsTemplate', // 模板组件名
category: 'savings', // 产品分类
config: { // 模板配置对象
// === 基础配置 ===
currency: 'USD', // 默认币种
payment_periods: [ // 缴费年期选项
'整付', '3 年', '5 年', '10 年', '15 年'
],
age_range: { min: 0, max: 100 }, // 投保年龄范围
insurance_period: '终身', // 保险期间
// === 提取计划配置(储蓄型产品) ===
withdrawal_plan: {
enabled: true, // 是否启用提取计划
currencies: ['HKD', 'USD', 'CNY'], // 支持的币种
default_currency: 'USD', // 默认币种
withdrawal_modes: [ // 提取模式
'指定提取金额',
'最高固定提取金额'
],
withdrawal_periods: [ // 提取年期
'1年', '2年', '3年', '5年',
'10年', '15年', '20年', '终身'
]
},
// === 表单 Schema ===
form_schema: savingsFormSchema, // 见下方详细说明
// === 提交字段映射 ===
submit_mapping: savingsSubmitMapping // 见下方详细说明
}
}
}
```
### 2. FormSchema(表单结构定义)
```javascript
// 位置: src/config/plan-templates.js
const savingsFormSchema = {
// 基础字段:所有产品都有的字段
base_fields: [
{
id: 'customer_name', // 唯一标识
key: 'customer_name', // formData 中的键名
type: 'name', // 字段类型
label: '申请人', // 显示标签
placeholder: '请输入申请人', // 占位符
required: true // 是否必填
},
{
id: 'gender',
key: 'gender',
type: 'radio', // 单选框
label: '性别',
options: ['男', '女'], // 选项列表
required: true
},
{
id: 'birthday',
key: 'birthday',
type: 'date', // 日期选择器
label: '出生年月日',
placeholder: '请选择年月日',
required: true
},
{
id: 'coverage',
key: 'coverage',
type: 'amount', // 金额输入(键盘)
label: '年缴保费',
placeholder: '请输入年缴保费',
input_label: '请输入年缴保费金额',
required: true,
currency_from: 'currency' // 币种来源: config.currency
},
{
id: 'payment_period',
key: 'payment_period',
type: 'payment_period', // 缴费年期(专用组件)
label: '缴费年期',
required: true,
options_from: 'payment_periods' // 选项来源: config.payment_periods
}
],
// 提取计划字段:储蓄型产品特有
withdrawal_fields: [
{
id: 'withdrawal_enabled',
key: 'withdrawal_enabled',
type: 'radio',
label: '是否希望生成一份允许减少名义金额的提取说明?',
options: ['是', '否'],
required: true,
default: '否'
},
{
id: 'withdrawal_mode',
key: 'withdrawal_mode',
type: 'radio',
label: '提取选项',
options: ['指定提取金额', '最高固定提取金额'],
required: true,
default: '指定提取金额',
section_title: '款项提取(允许减少名义金额)',
clear_when_hidden: true, // 隐藏时清空值
// === 条件显示规则 ===
show_when: {
field: 'withdrawal_enabled', // 依赖字段
op: 'eq', // 操作符: eq/ne/gt/lt/in
value: '是' // 比较值
}
},
{
id: 'withdrawal_method',
key: 'withdrawal_method',
type: 'radio',
label: '提取方式',
options: ['按年岁'],
required: true,
default: '按年岁',
show_when: {
field: 'withdrawal_mode',
op: 'eq',
value: '指定提取金额'
},
clear_when_hidden: true
},
{
id: 'annual_withdrawal_amount',
key: 'annual_withdrawal_amount',
type: 'amount',
label: '每年提取金额',
placeholder: '请输入每年提取金额',
input_label: '请输入每年提取金额',
required: true,
currency_from: 'withdrawal_plan.default_currency',
show_when: {
field: 'withdrawal_mode',
op: 'eq',
value: '指定提取金额'
},
clear_when_hidden: true
},
// ... 更多字段
]
}
```
### 3. SubmitMapping(提交字段映射)
```javascript
// 位置: src/config/plan-templates.js
const savingsSubmitMapping = {
// 基础字段映射
customer_name: {
api_field: 'customer_name' // API 字段名
},
gender: {
api_field: 'customer_gender'
},
birthday: {
api_field: 'customer_birthday'
},
smoker: {
api_field: 'smoking_status'
},
coverage: {
api_field: 'annual_premium', // API 字段名
transform: 'fen_to_yuan' // 值转换: 分→元
},
payment_period: {
api_field: 'payment_years'
},
// 提取计划字段映射
withdrawal_enabled: {
api_field: 'allow_reduce_amount'
},
withdrawal_mode: {
api_field: 'withdrawal_option'
},
annual_withdrawal_amount: {
api_field: 'annual_withdrawal_amount',
transform: 'fen_to_yuan'
},
// 指定提取金额模式的特殊字段
withdrawal_start_age_specified: {
api_field: 'withdrawal_start_age' // 根据模式动态选择
},
withdrawal_period_specified: {
api_field: 'withdrawal_period'
},
// 最高固定提取金额模式的特殊字段
withdrawal_start_age_fixed: {
api_field: 'withdrawal_start_age'
},
withdrawal_period_fixed: {
api_field: 'withdrawal_period'
}
}
```
### 4. FieldDefinitions(字段定义)
```javascript
// 位置: src/config/plan-fields.js
export const PLAN_FIELD_DEFINITIONS = {
customer_name: {
label: '申请人',
type: FIELD_TYPES.TEXT,
required: true,
api_field: 'customer_name',
component: 'PlanFieldName',
group: FIELD_GROUPS.BASIC,
validation: {
required: (value) => value?.trim()?.length >= 2
}
},
coverage: {
label: '保额',
type: FIELD_TYPES.AMOUNT,
required: true,
api_field: 'annual_premium',
transform: TRANSFORM_TYPES.FEN_TO_YUAN,
component: 'PlanFieldAmount',
group: FIELD_GROUPS.COVERAGE,
placeholder: '请输入保额',
validation: {
required: (value) => value > 0,
min: (value, config) => value >= (config?.min_coverage || 1000),
max: (value, config) => value <= (config?.max_coverage || 10000000)
}
},
// ... 更多字段定义
}
```
---
## 配置生成流程
### 1. 文档解析生成配置(当前工具)
```
┌─────────────────────────────────────────────────────────────────────────┐
│ 文档解析流程 │
│ │
│ 产品文档 (PDF/Word) │
│ │ │
│ ▼ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ AI 文档解析器 (/admin/document-parser/index) │ │
│ │ ├─ 豆包 AI / OpenAI API │ │
│ │ ├─ 智能字段提取 │ │
│ │ └─ 生成结构化配置 │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ 提取的配置数据 │ │
│ │ { │ │
│ │ product_name: "宏挚传承保障计划", │ │
│ │ product_type: "savings", │ │
│ │ currency: "USD", │ │
│ │ payment_periods: ["整付", "3 年", ...], │ │
│ │ form_schema: { base_fields: [...], withdrawal_fields: [...] }│ │
│ │ } │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ config-generator.js - 配置代码生成器 │ │
│ │ ├─ generateConfigCode() - 生成配置代码 │ │
│ │ ├─ generateSavingsConfig() - 生成储蓄型配置 │ │
│ │ └─ buildSchemaCode() - 构建 Schema 代码 │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 配置代码(复制到 plan-templates.js) │
└─────────────────────────────────────────────────────────────────────────┘
```
### 2. 运行时配置加载流程
```javascript
// PlanFormContainer.vue 中的关键代码
// 1. 根据 form_sn 获取模板配置
const templateConfig = computed(() => {
const config = PLAN_TEMPLATES[props.product.form_sn]
// 2. 合并后端返回的 plan_config(覆盖默认配置)
return {
...config.config, // 默认配置
...config, // 顶层属性(name, component 等)
...(props.product.plan_config || {}) // 后端动态配置
}
})
// 3. 动态加载模板组件
const currentTemplateComponent = computed(() => {
const componentMap = {
'LifeInsuranceTemplate': LifeInsuranceTemplate,
'CriticalIllnessTemplate': CriticalIllnessTemplate,
'SavingsTemplate': SavingsTemplate
}
return componentMap[templateConfig.value.component]
})
```
---
## 后端迁移方案
### 方案对比
| 方案 | 优势 | 劣势 | 推荐度 |
|------|------|------|--------|
| **方案 A:完全后端化** | 前端代码最少,配置集中管理 | 后端复杂度高,需要维护表单渲染引擎 | ⭐⭐⭐ |
| **方案 B:配置 API 化** | 前端保留灵活性,后端只管理配置 | 前端仍需维护模板组件 | ⭐⭐⭐⭐⭐ |
| **方案 C:混合模式** | 平衡前后端职责,渐进式迁移 | 架构稍复杂 | ⭐⭐⭐⭐ |
### 推荐方案:方案 B(配置 API 化)
```
┌─────────────────────────────────────────────────────────────────────────┐
│ 迁移后架构(方案 B) │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 后端配置管理 │ │
│ │ │ │
│ │ 产品配置表 (product_configs) │ │
│ │ ├─ form_sn │ │
│ │ ├─ form_schema (JSON) │ │
│ │ ├─ submit_mapping (JSON) │ │
│ │ └─ metadata (currency, age_range, ...) │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ │ GET /api/plan/config/:form_sn │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 前端配置缓存 │ │
│ │ │ │
│ │ Pinia Store (planConfigStore) │ │
│ │ ├─ configs: Map<form_sn, TemplateConfig> │ │
│ │ ├─ fetchConfig(formSn) - 拉取配置 │ │
│ │ └─ prefetchAll() - 预加载所有配置 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ PlanFormContainer │ │
│ │ (逻辑不变,只是配置来源变了) │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Template Components │ │
│ │ (保持不变) │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
```
---
## API 设计建议
### 1. 获取计划书配置
```http
GET /api/plan/config/:form_sn
```
**Response**:
```json
{
"code": 1,
"data": {
"form_sn": "savings-gs",
"name": "宏挚传承保障计划",
"component": "SavingsTemplate",
"config": {
"currency": "USD",
"payment_periods": ["整付", "3 年", "5 年"],
"age_range": { "min": 0, "max": 100 },
"insurance_period": "终身",
"withdrawal_plan": {
"enabled": true,
"currencies": ["HKD", "USD", "CNY"],
"default_currency": "USD",
"withdrawal_modes": ["指定提取金额", "最高固定提取金额"],
"withdrawal_periods": ["1年", "2年", "3年"]
},
"form_schema": {
"base_fields": [
{
"id": "customer_name",
"key": "customer_name",
"type": "name",
"label": "申请人",
"placeholder": "请输入申请人",
"required": true
}
// ... 更多字段
],
"withdrawal_fields": [
// ... 提取计划字段
]
},
"submit_mapping": {
"customer_name": { "api_field": "customer_name" },
"coverage": { "api_field": "annual_premium", "transform": "fen_to_yuan" }
// ... 更多映射
}
}
}
}
```
### 2. 批量获取配置(预加载)
```http
GET /api/plan/config/batch?form_sn[]=savings-gs&form_sn[]=life-insurance-wiop3e
```
**Response**:
```json
{
"code": 1,
"data": {
"savings-gs": { /* 配置对象 */ },
"life-insurance-wiop3e": { /* 配置对象 */ }
}
}
```
### 3. 创建计划书(保持不变)
```http
POST /srv/?a=proposal&t=add
```
**Request**:
```json
{
"product_id": 123,
"customer_name": "张三",
"customer_gender": "male",
"annual_premium": 100000, // 单位:元
"payment_years": "5 年",
"withdrawal_option": "指定提取金额",
"allow_reduce_amount": true
// ... 其他字段
}
```
---
## 数据模型设计
### product_configs 表
```sql
CREATE TABLE product_configs (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
form_sn VARCHAR(100) UNIQUE NOT NULL COMMENT '产品表单标识',
name VARCHAR(200) NOT NULL COMMENT '产品名称',
component VARCHAR(50) NOT NULL COMMENT '模板组件名',
category VARCHAR(50) COMMENT '产品分类',
-- 基础配置 (JSON)
config JSON NOT NULL COMMENT '模板配置',
-- 元数据 (冗余字段,方便查询)
currency VARCHAR(10) COMMENT '默认币种',
age_min INT COMMENT '最小投保年龄',
age_max INT COMMENT '最大投保年龄',
insurance_period VARCHAR(50) COMMENT '保险期间',
-- 时间戳
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_form_sn (form_sn),
INDEX idx_category (category)
) COMMENT='计划书产品配置表';
```
### config JSON 结构
```json
{
"currency": "USD",
"payment_periods": ["整付", "3 年", "5 年"],
"age_range": { "min": 0, "max": 100 },
"insurance_period": "终身",
"withdrawal_plan": {
"enabled": true,
"currencies": ["HKD", "USD", "CNY"],
"default_currency": "USD",
"withdrawal_modes": ["指定提取金额", "最高固定提取金额"],
"withdrawal_periods": ["1年", "2年", "3年"]
},
"form_schema": {
"base_fields": [
{
"id": "customer_name",
"key": "customer_name",
"type": "name",
"label": "申请人",
"placeholder": "请输入申请人",
"required": true
}
],
"withdrawal_fields": []
},
"submit_mapping": {
"customer_name": { "api_field": "customer_name" },
"coverage": { "api_field": "annual_premium", "transform": "fen_to_yuan" }
}
}
```
---
## 迁移步骤建议
### 阶段 1:后端配置存储(1-2 周)
- [ ] 创建 `product_configs`
- [ ] 迁移现有配置到数据库
- [ ] 实现配置 CRUD API
### 阶段 2:前端配置 API 集成(1 周)
- [ ] 创建 `planConfigStore`
- [ ] 修改 `PlanFormContainer` 使用 API 配置
- [ ] 添加配置缓存和预加载逻辑
### 阶段 3:配置管理后台(1-2 周)
- [ ] 开发配置管理界面
- [ ] 支持可视化配置表单字段
- [ ] 支持配置预览和测试
### 阶段 4:文档解析集成(1 周)
- [ ] 将文档解析器连接到配置 API
- [ ] 支持自动创建和更新配置
---
## 附录
### A. 字段类型与组件映射表
| field.type | 组件 | 说明 |
|------------|------|------|
| `name` | PlanFieldName | 姓名输入(特殊验证) |
| `text` | nut-input | 普通文本输入 |
| `amount` | PlanFieldAmount | 金额键盘输入 |
| `percentage` | nut-input | 百分比输入(0-100) |
| `date` | PlanFieldDatePicker | 日期选择器 |
| `age` | PlanFieldAgePicker | 年龄选择器 |
| `radio` | PlanFieldRadio | 单选按钮组 |
| `select` | PlanFieldSelect | 下拉选择器 |
| `payment_period` | PaymentPeriodRadio | 缴费年期(专用) |
### B. 条件操作符
| 操作符 | 说明 | 示例 |
|--------|------|------|
| `eq` | 等于 | `{ field: 'gender', op: 'eq', value: 'male' }` |
| `ne` | 不等于 | `{ field: 'gender', op: 'ne', value: 'male' }` |
| `gt` | 大于 | `{ field: 'age', op: 'gt', value: 18 }` |
| `lt` | 小于 | `{ field: 'age', op: 'lt', value: 65 }` |
| `in` | 包含于 | `{ field: 'type', op: 'in', value: ['A', 'B'] }` |
### C. 值转换类型
| transform | 说明 | 方向 |
|-----------|------|------|
| `fen_to_yuan` | 分 → 元 | 提交时 |
| `yuan_to_fen` | 元 → 分 | 回显时 |
| `none` | 不转换 | - |
---
## 相关文档
- [计划书录入架构设计](./plan-entry-architecture.md)
- [表单 Schema 使用指南](./plan-form-schema-usage.md)
- [快速上手指南](./plan-entry-quick-guide.md)
- [字段依赖管理](../composables/plan-field-dependencies.md)
# 计划书配置 JSON Schema 规范
> **文档目的**:定义计划书配置的完整 JSON Schema,用于后端验证和前端类型检查
**作者**:Claude Code
**创建日期**:2026-02-25
**版本**:1.0.0
---
## 目录
1. [配置根对象](#配置根对象)
2. [FormSchema 规范](#formschema-规范)
3. [SubmitMapping 规范](#submitmapping-规范)
4. [字段定义规范](#字段定义规范)
5. [条件规则规范](#条件规则规范)
---
## 配置根对象
### TemplateConfig
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "计划书模板配置",
"type": "object",
"required": ["form_sn", "name", "component", "config"],
"properties": {
"form_sn": {
"type": "string",
"description": "产品唯一标识,格式: {category}-{product_code}",
"pattern": "^(life-insurance|critical-illness|savings)-[a-z0-9-]+$",
"examples": ["savings-gs", "life-insurance-wiop3e"]
},
"name": {
"type": "string",
"description": "产品名称",
"examples": ["宏挚传承保障计划", "WIOP3E 盈传创富保障计划 3"]
},
"component": {
"type": "string",
"enum": ["LifeInsuranceTemplate", "CriticalIllnessTemplate", "SavingsTemplate"],
"description": "对应的模板组件名"
},
"category": {
"type": "string",
"enum": ["life-insurance", "critical-illness", "savings"],
"description": "产品分类"
},
"config": {
"$ref": "#/definitions/TemplateConfigData"
}
},
"definitions": {
"TemplateConfigData": {
"type": "object",
"required": ["currency", "payment_periods", "age_range", "form_schema"],
"properties": {
"currency": {
"$ref": "#/definitions/CurrencyCode"
},
"payment_periods": {
"type": "array",
"items": { "type": "string" },
"description": "缴费年期选项",
"examples": [["整付", "5 年", "10 年"]]
},
"age_range": {
"$ref": "#/definitions/AgeRange"
},
"insurance_period": {
"type": "string",
"description": "保险期间",
"examples": ["终身", "至 100 岁"]
},
"withdrawal_plan": {
"$ref": "#/definitions/WithdrawalPlan"
},
"form_schema": {
"$ref": "#/definitions/FormSchema"
},
"submit_mapping": {
"$ref": "#/definitions/SubmitMapping"
}
}
}
}
}
```
### 辅助定义
```json
{
"definitions": {
"CurrencyCode": {
"type": "string",
"enum": ["CNY", "USD", "HKD", "EUR"],
"description": "币种代码"
},
"AgeRange": {
"type": "object",
"properties": {
"min": {
"type": "integer",
"minimum": 0,
"description": "最小年龄"
},
"max": {
"type": "integer",
"maximum": 150,
"description": "最大年龄"
}
},
"required": ["min", "max"]
}
}
}
```
---
## FormSchema 规范
### FormSchema 根对象
```json
{
"FormSchema": {
"type": "object",
"properties": {
"base_fields": {
"type": "array",
"items": { "$ref": "#/definitions/FieldDefinition" },
"description": "基础字段(所有产品共有)"
},
"withdrawal_fields": {
"type": "array",
"items": { "$ref": "#/definitions/FieldDefinition" },
"description": "提取计划字段(储蓄型产品特有)"
}
},
"required": ["base_fields"]
}
}
```
---
## 字段定义规范
### FieldDefinition
```json
{
"FieldDefinition": {
"type": "object",
"required": ["id", "key", "type", "label"],
"properties": {
"id": {
"type": "string",
"description": "字段唯一标识(用于 v-for key)"
},
"key": {
"type": "string",
"description": "formData 中的键名"
},
"type": {
"$ref": "#/definitions/FieldType",
"description": "字段类型(决定使用哪个组件)"
},
"label": {
"type": "string",
"description": "字段显示标签"
},
"placeholder": {
"type": "string",
"description": "占位符文本"
},
"input_label": {
"type": "string",
"description": "金额键盘弹窗标题(amount 类型专用)"
},
"required": {
"type": "boolean",
"description": "是否必填",
"default": false
},
"default": {
"description": "默认值",
"oneOf": [
{ "type": "string" },
{ "type": "boolean" },
{ "type": "number" }
]
},
"options": {
"type": "array",
"items": { "type": "string" },
"description": "选项列表(radio/select 类型)"
},
"options_from": {
"type": "string",
"description": "选项来源配置引用",
"examples": ["payment_periods", "withdrawal_plan.withdrawal_periods"]
},
"currency_from": {
"type": "string",
"description": "币种来源配置引用",
"examples": ["currency", "withdrawal_plan.default_currency"]
},
"section_title": {
"type": "string",
"description": "分组标题(用于字段分组显示)"
},
"show_when": {
"$ref": "#/definitions/ConditionRule",
"description": "条件显示规则"
},
"clear_when_hidden": {
"description": "隐藏时是否清空值",
"oneOf": [
{ "type": "boolean" },
{ "type": "null" },
{
"type": "object",
"properties": {
"clear_self": { "type": "boolean" },
"clear_dependents": {
"type": "array",
"items": { "type": "string" }
}
}
}
]
}
}
}
}
```
### FieldType 枚举
```json
{
"FieldType": {
"type": "string",
"enum": [
"name",
"text",
"amount",
"percentage",
"date",
"age",
"radio",
"select",
"payment_period"
],
"description": "字段类型枚举"
}
}
```
### 字段类型与组件映射
| type | 组件 | 说明 | options_from 支持 |
|------|------|------|-------------------|
| `name` | PlanFieldName | 姓名输入 | ❌ |
| `text` | nut-input | 普通文本 | ❌ |
| `amount` | PlanFieldAmount | 金额键盘 | ✅ currency_from |
| `percentage` | nut-input | 百分比 (0-100) | ❌ |
| `date` | PlanFieldDatePicker | 日期选择器 | ❌ |
| `age` | PlanFieldAgePicker | 年龄选择器 | ❌ |
| `radio` | PlanFieldRadio | 单选按钮 | ✅ options_from |
| `select` | PlanFieldSelect | 下拉选择器 | ✅ options_from |
| `payment_period` | PaymentPeriodRadio | 缴费年期专用 | ✅ options_from |
---
## 条件规则规范
### ConditionRule
```json
{
"ConditionRule": {
"oneOf": [
{ "$ref": "#/definitions/SimpleCondition" },
{ "$ref": "#/definitions/CompositeCondition" }
]
},
"SimpleCondition": {
"type": "object",
"required": ["field", "op", "value"],
"properties": {
"field": {
"type": "string",
"description": "依赖的字段 key"
},
"op": {
"$ref": "#/definitions/ConditionOperator",
"description": "比较操作符"
},
"value": {
"description": "比较值",
"oneOf": [
{ "type": "string" },
{ "type": "number" },
{ "type": "boolean" },
{
"type": "array",
"items": { "type": "string" }
}
]
}
}
},
"CompositeCondition": {
"type": "object",
"required": ["op", "conditions"],
"properties": {
"op": {
"enum": ["AND", "OR"],
"description": "逻辑操作符"
},
"conditions": {
"type": "array",
"items": { "$ref": "#/definitions/ConditionRule" },
"description": "子条件列表"
}
}
},
"ConditionOperator": {
"type": "string",
"enum": ["eq", "ne", "gt", "lt", "gte", "lte", "in"],
"description": "比较操作符"
}
}
```
### 条件规则示例
```json
{
"简单条件": {
"field": "withdrawal_enabled",
"op": "eq",
"value": true
},
"字符串相等": {
"field": "withdrawal_mode",
"op": "eq",
"value": "指定提取金额"
},
"数值范围": {
"field": "age",
"op": "gte",
"value": 18
},
"包含于": {
"field": "product_type",
"op": "in",
"value": ["A", "B", "C"]
},
"复合条件": {
"op": "AND",
"conditions": [
{ "field": "age", "op": "gte", "value": 18 },
{ "field": "age", "op": "lt", "value": 65 }
]
}
}
```
### 向后兼容的旧格式
```json
{
"旧格式(自动转换)": {
"field": "withdrawal_mode",
"equals": "指定提取金额"
},
"新格式": {
"field": "withdrawal_mode",
"op": "eq",
"value": "指定提取金额"
}
}
```
---
## SubmitMapping 规范
### SubmitMapping 根对象
```json
{
"SubmitMapping": {
"type": "object",
"patternProperties": {
"^[a-z_]+$": {
"oneOf": [
{ "type": "string" },
{ "$ref": "#/definitions/FieldMapping" }
]
}
},
"description": "字段键名到 API 字段映射的字典"
},
"FieldMapping": {
"type": "object",
"required": ["api_field"],
"properties": {
"api_field": {
"type": "string",
"description": "API 接口字段名"
},
"transform": {
"type": "string",
"enum": ["fen_to_yuan", "yuan_to_fen", "none"],
"description": "值转换类型",
"default": "none"
}
}
}
}
```
### SubmitMapping 示例
```json
{
"customer_name": {
"api_field": "customer_name"
},
"gender": {
"api_field": "customer_gender"
},
"coverage": {
"api_field": "annual_premium",
"transform": "fen_to_yuan"
},
"annual_withdrawal_amount": {
"api_field": "annual_withdrawal_amount",
"transform": "fen_to_yuan"
},
"withdrawal_start_age_specified": {
"api_field": "withdrawal_start_age"
},
"withdrawal_start_age_fixed": {
"api_field": "withdrawal_start_age"
}
}
```
### 简写格式
```json
{
"简写格式(无需转换)": "customer_name",
"等价于": {
"api_field": "customer_name"
}
}
```
---
## WithdrawalPlan 规范
```json
{
"WithdrawalPlan": {
"type": "object",
"required": ["enabled"],
"properties": {
"enabled": {
"type": "boolean",
"description": "是否启用提取计划"
},
"currencies": {
"type": "array",
"items": { "$ref": "#/definitions/CurrencyCode" },
"description": "支持的币种列表"
},
"default_currency": {
"$ref": "#/definitions/CurrencyCode",
"description": "默认币种"
},
"withdrawal_modes": {
"type": "array",
"items": { "type": "string" },
"description": "提取模式选项",
"examples": [["指定提取金额", "最高固定提取金额"]]
},
"withdrawal_periods": {
"type": "array",
"items": { "type": "string" },
"description": "提取年期选项",
"examples": [["1年", "2年", "5年", "10年", "终身"]]
}
}
}
}
```
---
## 完整配置示例
### 储蓄型产品配置
```json
{
"form_sn": "savings-gs",
"name": "宏挚传承保障计划",
"component": "SavingsTemplate",
"category": "savings",
"config": {
"currency": "USD",
"payment_periods": ["整付", "3 年", "5 年", "10 年", "15 年"],
"age_range": { "min": 0, "max": 100 },
"insurance_period": "终身",
"withdrawal_plan": {
"enabled": true,
"currencies": ["HKD", "USD", "CNY"],
"default_currency": "USD",
"withdrawal_modes": ["指定提取金额", "最高固定提取金额"],
"withdrawal_periods": ["1年", "2年", "3年", "5年", "10年", "15年", "20年", "终身"]
},
"form_schema": {
"base_fields": [
{
"id": "customer_name",
"key": "customer_name",
"type": "name",
"label": "申请人",
"placeholder": "请输入申请人",
"required": true
},
{
"id": "gender",
"key": "gender",
"type": "radio",
"label": "性别",
"options": ["男", "女"],
"required": true
},
{
"id": "coverage",
"key": "coverage",
"type": "amount",
"label": "年缴保费",
"placeholder": "请输入年缴保费",
"input_label": "请输入年缴保费金额",
"required": true,
"currency_from": "currency"
},
{
"id": "payment_period",
"key": "payment_period",
"type": "payment_period",
"label": "缴费年期",
"required": true,
"options_from": "payment_periods"
}
],
"withdrawal_fields": [
{
"id": "withdrawal_enabled",
"key": "withdrawal_enabled",
"type": "radio",
"label": "是否希望生成一份允许减少名义金额的提取说明?",
"options": ["是", "否"],
"required": true,
"default": "否"
},
{
"id": "withdrawal_mode",
"key": "withdrawal_mode",
"type": "radio",
"label": "提取选项",
"options": ["指定提取金额", "最高固定提取金额"],
"required": true,
"default": "指定提取金额",
"section_title": "款项提取(允许减少名义金额)",
"clear_when_hidden": true,
"show_when": {
"field": "withdrawal_enabled",
"op": "eq",
"value": "是"
}
},
{
"id": "annual_withdrawal_amount",
"key": "annual_withdrawal_amount",
"type": "amount",
"label": "每年提取金额",
"placeholder": "请输入每年提取金额",
"input_label": "请输入每年提取金额",
"required": true,
"currency_from": "withdrawal_plan.default_currency",
"clear_when_hidden": true,
"show_when": {
"field": "withdrawal_mode",
"op": "eq",
"value": "指定提取金额"
}
}
]
},
"submit_mapping": {
"customer_name": { "api_field": "customer_name" },
"gender": { "api_field": "customer_gender" },
"coverage": { "api_field": "annual_premium", "transform": "fen_to_yuan" },
"payment_period": { "api_field": "payment_years" },
"withdrawal_enabled": { "api_field": "allow_reduce_amount" },
"withdrawal_mode": { "api_field": "withdrawal_option" },
"annual_withdrawal_amount": { "api_field": "annual_withdrawal_amount", "transform": "fen_to_yuan" }
}
}
}
```
---
## 验证工具
### Python JSON Schema 验证
```python
import json
from jsonschema import validate, ValidationError
# 定义 Schema (使用上面定义的 JSON Schema)
PLAN_CONFIG_SCHEMA = {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "计划书模板配置",
"type": "object",
"required": ["form_sn", "name", "component", "config"],
"properties": {
"form_sn": {
"type": "string",
"pattern": "^(life-insurance|critical-illness|savings)-[a-z0-9-]+$"
},
"name": { "type": "string" },
"component": {
"type": "string",
"enum": ["LifeInsuranceTemplate", "CriticalIllnessTemplate", "SavingsTemplate"]
},
"config": { "type": "object" }
}
}
def validate_plan_config(config):
"""验证计划书配置"""
try:
validate(instance=config, schema=PLAN_CONFIG_SCHEMA)
return True, "配置有效"
except ValidationError as e:
return False, f"验证失败: {e.message}"
# 使用示例
with open('plan-config.json') as f:
config = json.load(f)
is_valid, message = validate_plan_config(config)
print(f"验证结果: {is_valid}, {message}")
```
### JavaScript JSON Schema 验证
```javascript
import Ajv from 'ajv'
const schema = {
type: 'object',
required: ['form_sn', 'name', 'component', 'config'],
properties: {
form_sn: {
type: 'string',
pattern: '^(life-insurance|critical-illness|savings)-[a-z0-9-]+$'
},
name: { type: 'string' },
component: {
type: 'string',
enum: ['LifeInsuranceTemplate', 'CriticalIllnessTemplate', 'SavingsTemplate']
},
config: { type: 'object' }
}
}
const ajv = new Ajv()
const validate = ajv.compile(schema)
function validatePlanConfig(config) {
const valid = validate(config)
if (!valid) {
console.error('验证失败:', validate.errors)
return false
}
return true
}
// 使用示例
const config = { /* ... */ }
console.log(validatePlanConfig(config))
```
---
## TypeScript 类型定义
```typescript
/**
* 计划书配置类型定义
* 用于前端类型检查和后端 API 接口定义
*/
/** 币种代码 */
type CurrencyCode = 'CNY' | 'USD' | 'HKD' | 'EUR'
/** 字段类型 */
type FieldType =
| 'name'
| 'text'
| 'amount'
| 'percentage'
| 'date'
| 'age'
| 'radio'
| 'select'
| 'payment_period'
/** 条件操作符 */
type ConditionOperator = 'eq' | 'ne' | 'gt' | 'lt' | 'gte' | 'lte' | 'in'
/** 值转换类型 */
type TransformType = 'fen_to_yuan' | 'yuan_to_fen' | 'none'
/** 年龄范围 */
interface AgeRange {
min: number
max: number
}
/** 提取计划配置 */
interface WithdrawalPlan {
enabled: boolean
currencies?: CurrencyCode[]
default_currency?: CurrencyCode
withdrawal_modes?: string[]
withdrawal_periods?: string[]
}
/** 条件规则 */
type ConditionRule = SimpleCondition | CompositeCondition
interface SimpleCondition {
field: string
op: ConditionOperator
value: string | number | boolean | string[]
}
interface CompositeCondition {
op: 'AND' | 'OR'
conditions: ConditionRule[]
}
/** 清空隐藏配置 */
type ClearWhenHidden =
| boolean
| null
| {
clear_self?: boolean
clear_dependents?: string[]
}
/** 字段定义 */
interface FieldDefinition {
id: string
key: string
type: FieldType
label: string
placeholder?: string
input_label?: string
required?: boolean
default?: string | boolean | number
options?: string[]
options_from?: string
currency_from?: string
section_title?: string
show_when?: ConditionRule
clear_when_hidden?: ClearWhenHidden
}
/** 表单 Schema */
interface FormSchema {
base_fields: FieldDefinition[]
withdrawal_fields?: FieldDefinition[]
}
/** 字段映射 */
interface FieldMapping {
api_field: string
transform?: TransformType
}
/** 提交映射(字典) */
type SubmitMapping = Record<string, string | FieldMapping>
/** 模板配置数据 */
interface TemplateConfigData {
currency: CurrencyCode
payment_periods: string[]
age_range: AgeRange
insurance_period?: string
withdrawal_plan?: WithdrawalPlan
form_schema: FormSchema
submit_mapping: SubmitMapping
}
/** 模板配置(完整) */
interface TemplateConfig {
form_sn: string
name: string
component: 'LifeInsuranceTemplate' | 'CriticalIllnessTemplate' | 'SavingsTemplate'
category?: 'life-insurance' | 'critical-illness' | 'savings'
config: TemplateConfigData
}
```
---
## 相关文档
- [后端迁移指南](./plan-backend-migration-guide.md)
- [录入架构设计](./plan-entry-architecture.md)
- [Schema 使用指南](./plan-form-schema-usage.md)