docs(plan): 添加计划书配置架构文档和后端迁移指南
- 新增计划书配置架构详解文档 - 新增后端迁移指南(含数据流图、API设计、实施步骤) - 新增配置 Schema 参考文档 - 新增 Draw.io 架构可视化图 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Showing
3 changed files
with
1877 additions
and
0 deletions
| 1 | +<mxfile host="app.diagrams.net" modified="2026-02-25T00:00:00.000Z" agent="5.0" version="24.0.0"> | ||
| 2 | + <diagram id="plan-migration" name="计划书后端迁移架构"> | ||
| 3 | + <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"> | ||
| 4 | + <root> | ||
| 5 | + <mxCell id="0"/> | ||
| 6 | + <mxCell id="1" parent="0"/> | ||
| 7 | + | ||
| 8 | + <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"> | ||
| 9 | + <mxGeometry x="450" y="20" width="500" height="50" as="geometry"/> | ||
| 10 | + </mxCell> | ||
| 11 | + | ||
| 12 | + <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"> | ||
| 13 | + <mxGeometry x="60" y="100" width="560" height="680" as="geometry"/> | ||
| 14 | + </mxCell> | ||
| 15 | + | ||
| 16 | + <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"> | ||
| 17 | + <mxGeometry y="40" width="560" height="180" as="geometry"/> | ||
| 18 | + </mxCell> | ||
| 19 | + | ||
| 20 | + <mxCell id="plan-templates" value="plan-templates.js
产品映射: form_sn → TemplateConfig
基础 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"> | ||
| 21 | + <mxGeometry y="40" width="560" height="50" as="geometry"/> | ||
| 22 | + </mxCell> | ||
| 23 | + | ||
| 24 | + <mxCell id="plan-fields" value="plan-fields.js
字段类型定义、验证规则、字段分组" 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"> | ||
| 25 | + <mxGeometry y="100" width="560" height="50" as="geometry"/> | ||
| 26 | + </mxCell> | ||
| 27 | + | ||
| 28 | + <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"> | ||
| 29 | + <mxGeometry y="220" width="560" height="140" as="geometry"/> | ||
| 30 | + </mxCell> | ||
| 31 | + | ||
| 32 | + <mxCell id="plan-form-container" value="PlanFormContainer.vue
1. 根据 form_sn 获取配置
2. 合并 product.plan_config
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"> | ||
| 33 | + <mxGeometry y="40" width="560" height="100" as="geometry"/> | ||
| 34 | + </mxCell> | ||
| 35 | + | ||
| 36 | + <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"> | ||
| 37 | + <mxGeometry y="360" width="560" height="180" as="geometry"/> | ||
| 38 | + </mxCell> | ||
| 39 | + | ||
| 40 | + <mxCell id="life-template" value="LifeInsuranceTemplate
人寿保险模板" 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"> | ||
| 41 | + <mxGeometry y="40" width="170" height="50" as="geometry"/> | ||
| 42 | + </mxCell> | ||
| 43 | + | ||
| 44 | + <mxCell id="critical-template" value="CriticalIllnessTemplate
重疾保险模板" 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"> | ||
| 45 | + <mxGeometry y="100" width="170" height="50" as="geometry"/> | ||
| 46 | + </mxCell> | ||
| 47 | + | ||
| 48 | + <mxCell id="savings-template" value="SavingsTemplate
储蓄型保险模板" 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"> | ||
| 49 | + <mxGeometry y="160" width="170" height="20" as="geometry"/> | ||
| 50 | + </mxCell> | ||
| 51 | + | ||
| 52 | + <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"> | ||
| 53 | + <mxGeometry y="540" width="560" height="100" as="geometry"/> | ||
| 54 | + </mxCell> | ||
| 55 | + | ||
| 56 | + <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"> | ||
| 57 | + <mxGeometry y="40" width="560" height="60" as="geometry"/> | ||
| 58 | + </mxCell> | ||
| 59 | + | ||
| 60 | + <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"> | ||
| 61 | + <mxGeometry x="680" y="100" width="560" height="680" as="geometry"/> | ||
| 62 | + </mxCell> | ||
| 63 | + | ||
| 64 | + <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"> | ||
| 65 | + <mxGeometry y="40" width="560" height="140" as="geometry"/> | ||
| 66 | + </mxCell> | ||
| 67 | + | ||
| 68 | + <mxCell id="product-table" value="product_configs 表

form_sn (PK) | name | component
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"> | ||
| 69 | + <mxGeometry y="40" width="560" height="100" as="geometry"/> | ||
| 70 | + </mxCell> | ||
| 71 | + | ||
| 72 | + <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"> | ||
| 73 | + <mxGeometry y="180" width="560" height="240" as="geometry"/> | ||
| 74 | + </mxCell> | ||
| 75 | + | ||
| 76 | + <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"> | ||
| 77 | + <mxGeometry y="40" width="560" height="40" as="geometry"/> | ||
| 78 | + </mxCell> | ||
| 79 | + | ||
| 80 | + <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"> | ||
| 81 | + <mxGeometry y="80" width="560" height="40" as="geometry"/> | ||
| 82 | + </mxCell> | ||
| 83 | + | ||
| 84 | + <mxCell id="api-add" value="POST /srv/?a=proposal&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"> | ||
| 85 | + <mxGeometry y="120" width="560" height="40" as="geometry"/> | ||
| 86 | + </mxCell> | ||
| 87 | + | ||
| 88 | + <mxCell id="api-list" value="GET /srv/?a=proposal&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"> | ||
| 89 | + <mxGeometry y="160" width="560" height="40" as="geometry"/> | ||
| 90 | + </mxCell> | ||
| 91 | + | ||
| 92 | + <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"> | ||
| 93 | + <mxGeometry y="420" width="560" height="180" as="geometry"/> | ||
| 94 | + </mxCell> | ||
| 95 | + | ||
| 96 | + <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"> | ||
| 97 | + <mxGeometry y="40" width="560" height="40" as="geometry"/> | ||
| 98 | + </mxCell> | ||
| 99 | + | ||
| 100 | + <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"> | ||
| 101 | + <mxGeometry y="80" width="560" height="40" as="geometry"/> | ||
| 102 | + </mxCell> | ||
| 103 | + | ||
| 104 | + <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"> | ||
| 105 | + <mxGeometry y="120" width="560" height="40" as="geometry"/> | ||
| 106 | + </mxCell> | ||
| 107 | + | ||
| 108 | + <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"> | ||
| 109 | + <mxGeometry relative="1" as="geometry"/> | ||
| 110 | + </mxCell> | ||
| 111 | + | ||
| 112 | + <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"> | ||
| 113 | + <mxGeometry x="-0.1" y="1" relative="1" as="geometry"> | ||
| 114 | + <mxPoint y="-5" as="offset"/> | ||
| 115 | + </mxGeometry> | ||
| 116 | + </mxCell> | ||
| 117 | + | ||
| 118 | + <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"> | ||
| 119 | + <mxGeometry relative="1" as="geometry"/> | ||
| 120 | + </mxCell> | ||
| 121 | + | ||
| 122 | + <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"> | ||
| 123 | + <mxGeometry x="-0.1" y="1" relative="1" as="geometry"> | ||
| 124 | + <mxPoint y="-5" as="offset"/> | ||
| 125 | + </mxGeometry> | ||
| 126 | + </mxCell> | ||
| 127 | + | ||
| 128 | + <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"> | ||
| 129 | + <mxGeometry relative="1" as="geometry"/> | ||
| 130 | + </mxCell> | ||
| 131 | + | ||
| 132 | + <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"> | ||
| 133 | + <mxGeometry x="-0.1" y="1" relative="1" as="geometry"> | ||
| 134 | + <mxPoint y="-5" as="offset"/> | ||
| 135 | + </mxGeometry> | ||
| 136 | + </mxCell> | ||
| 137 | + | ||
| 138 | + <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"> | ||
| 139 | + <mxGeometry relative="1" as="geometry"> | ||
| 140 | + <Array as="points"> | ||
| 141 | + <mxPoint x="620" y="340"/> | ||
| 142 | + <mxPoint x="620" y="270"/> | ||
| 143 | + </Array> | ||
| 144 | + </mxGeometry> | ||
| 145 | + </mxCell> | ||
| 146 | + | ||
| 147 | + <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"> | ||
| 148 | + <mxGeometry x="-0.2" y="1" relative="1" as="geometry"> | ||
| 149 | + <mxPoint y="-10" as="offset"/> | ||
| 150 | + </mxGeometry> | ||
| 151 | + </mxCell> | ||
| 152 | + | ||
| 153 | + <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"> | ||
| 154 | + <mxGeometry relative="1" as="geometry"> | ||
| 155 | + <Array as="points"> | ||
| 156 | + <mxPoint x="620" y="380"/> | ||
| 157 | + <mxPoint x="620" y="320"/> | ||
| 158 | + </Array> | ||
| 159 | + </mxGeometry> | ||
| 160 | + </mxCell> | ||
| 161 | + | ||
| 162 | + <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"> | ||
| 163 | + <mxGeometry x="-0.15" y="1" relative="1" as="geometry"> | ||
| 164 | + <mxPoint y="-5" as="offset"/> | ||
| 165 | + </mxGeometry> | ||
| 166 | + </mxCell> | ||
| 167 | + | ||
| 168 | + <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"> | ||
| 169 | + <mxGeometry relative="1" as="geometry"/> | ||
| 170 | + </mxCell> | ||
| 171 | + | ||
| 172 | + <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"> | ||
| 173 | + <mxGeometry relative="1" as="geometry"> | ||
| 174 | + <Array as="points"> | ||
| 175 | + <mxPoint x="1240" y="520"/> | ||
| 176 | + </Array> | ||
| 177 | + </mxGeometry> | ||
| 178 | + </mxCell> | ||
| 179 | + | ||
| 180 | + <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"> | ||
| 181 | + <mxGeometry x="60" y="820" width="1180" height="200" as="geometry"/> | ||
| 182 | + </mxCell> | ||
| 183 | + | ||
| 184 | + <mxCell id="solution-a" value="方案 A:完全后端化
优势 前端代码最少 配置集中
劣势 后端复杂度高 需维护表单引擎
推荐 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"> | ||
| 185 | + <mxGeometry y="40" width="360" height="60" as="geometry"/> | ||
| 186 | + </mxCell> | ||
| 187 | + | ||
| 188 | + <mxCell id="solution-b" value="方案 B:配置 API 化 推荐
优势 前端保留灵活性 后端只管理配置
劣势 前端仍需维护模板组件
推荐 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"> | ||
| 189 | + <mxGeometry y="100" width="360" height="60" as="geometry"/> | ||
| 190 | + </mxCell> | ||
| 191 | + | ||
| 192 | + <mxCell id="solution-c" value="方案 C:混合模式
优势 平衡前后端职责 渐进式迁移
劣势 架构稍复杂
推荐 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"> | ||
| 193 | + <mxGeometry y="160" width="360" height="40" as="geometry"/> | ||
| 194 | + </mxCell> | ||
| 195 | + | ||
| 196 | + <mxCell id="migration-steps" value="迁移步骤 4 个阶段

阶段 1 后端配置存储 1-2 周 创建表 迁移配置 CRUD API
阶段 2 前端 API 集成 1 周 Store 容器 缓存
阶段 3 配置管理后台 1-2 周 可视化 预览 测试
阶段 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"> | ||
| 197 | + <mxGeometry x="380" y="40" width="780" height="160" as="geometry"/> | ||
| 198 | + </mxCell> | ||
| 199 | + | ||
| 200 | + <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"> | ||
| 201 | + <mxGeometry x="60" y="1050" width="1180" height="100" as="geometry"/> | ||
| 202 | + </mxCell> | ||
| 203 | + | ||
| 204 | + <mxCell id="legend-config" value="" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;strokeWidth=1;" vertex="1" parent="legend-box"> | ||
| 205 | + <mxGeometry x="10" y="45" width="25" height="25" as="geometry"/> | ||
| 206 | + </mxCell> | ||
| 207 | + | ||
| 208 | + <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"> | ||
| 209 | + <mxGeometry x="45" y="45" width="90" height="25" as="geometry"/> | ||
| 210 | + </mxCell> | ||
| 211 | + | ||
| 212 | + <mxCell id="legend-component" value="" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;strokeWidth=1;" vertex="1" parent="legend-box"> | ||
| 213 | + <mxGeometry x="150" y="45" width="25" height="25" as="geometry"/> | ||
| 214 | + </mxCell> | ||
| 215 | + | ||
| 216 | + <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"> | ||
| 217 | + <mxGeometry x="185" y="45" width="90" height="25" as="geometry"/> | ||
| 218 | + </mxCell> | ||
| 219 | + | ||
| 220 | + <mxCell id="legend-template" value="" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;strokeWidth=1;" vertex="1" parent="legend-box"> | ||
| 221 | + <mxGeometry x="290" y="45" width="25" height="25" as="geometry"/> | ||
| 222 | + </mxCell> | ||
| 223 | + | ||
| 224 | + <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"> | ||
| 225 | + <mxGeometry x="325" y="45" width="90" height="25" as="geometry"/> | ||
| 226 | + </mxCell> | ||
| 227 | + | ||
| 228 | + <mxCell id="legend-field" value="" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;strokeWidth=1;" vertex="1" parent="legend-box"> | ||
| 229 | + <mxGeometry x="430" y="45" width="25" height="25" as="geometry"/> | ||
| 230 | + </mxCell> | ||
| 231 | + | ||
| 232 | + <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"> | ||
| 233 | + <mxGeometry x="465" y="45" width="90" height="25" as="geometry"/> | ||
| 234 | + </mxCell> | ||
| 235 | + | ||
| 236 | + <mxCell id="legend-api" value="" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffffff;strokeColor=#82b366;strokeWidth=1;" vertex="1" parent="legend-box"> | ||
| 237 | + <mxGeometry x="570" y="45" width="25" height="25" as="geometry"/> | ||
| 238 | + </mxCell> | ||
| 239 | + | ||
| 240 | + <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"> | ||
| 241 | + <mxGeometry x="605" y="45" width="90" height="25" as="geometry"/> | ||
| 242 | + </mxCell> | ||
| 243 | + | ||
| 244 | + <mxCell id="legend-arrow-solid" value="" style="endArrow=classic;html=1;strokeWidth=3;strokeColor=#666666;" edge="1" parent="legend-box"> | ||
| 245 | + <mxGeometry width="60" height="40" relative="1" as="geometry"> | ||
| 246 | + <mxPoint x="730" y="55" as="sourcePoint"/> | ||
| 247 | + <mxPoint x="790" y="55" as="targetPoint"/> | ||
| 248 | + </mxGeometry> | ||
| 249 | + </mxCell> | ||
| 250 | + | ||
| 251 | + <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"> | ||
| 252 | + <mxGeometry x="800" y="45" width="90" height="25" as="geometry"/> | ||
| 253 | + </mxCell> | ||
| 254 | + | ||
| 255 | + <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"> | ||
| 256 | + <mxGeometry width="60" height="40" relative="1" as="geometry"> | ||
| 257 | + <mxPoint x="910" y="55" as="sourcePoint"/> | ||
| 258 | + <mxPoint x="970" y="55" as="targetPoint"/> | ||
| 259 | + </mxGeometry> | ||
| 260 | + </mxCell> | ||
| 261 | + | ||
| 262 | + <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"> | ||
| 263 | + <mxGeometry x="980" y="45" width="100" height="25" as="geometry"/> | ||
| 264 | + </mxCell> | ||
| 265 | + | ||
| 266 | + <mxCell id="legend-arrow-green" value="" style="endArrow=classic;html=1;strokeWidth=4;strokeColor=#82b366;" edge="1" parent="legend-box"> | ||
| 267 | + <mxGeometry width="60" height="40" relative="1" as="geometry"> | ||
| 268 | + <mxPoint x="1100" y="55" as="sourcePoint"/> | ||
| 269 | + <mxPoint x="1160" y="55" as="targetPoint"/> | ||
| 270 | + </mxGeometry> | ||
| 271 | + </mxCell> | ||
| 272 | + | ||
| 273 | + <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"> | ||
| 274 | + <mxGeometry x="1170" y="45" width="90" height="25" as="geometry"/> | ||
| 275 | + </mxCell> | ||
| 276 | + | ||
| 277 | + </root> | ||
| 278 | + </mxGraphModel> | ||
| 279 | + </diagram> | ||
| 280 | +</mxfile> |
docs/PLAN/plan-backend-migration-guide.md
0 → 100644
| 1 | +# 计划书配置架构与后端迁移指南 | ||
| 2 | + | ||
| 3 | +> **文档目的**:详细说明计划书配置生成逻辑,为后端迁移提供完整的技术参考 | ||
| 4 | + | ||
| 5 | +**作者**:Claude Code | ||
| 6 | +**创建日期**:2026-02-25 | ||
| 7 | +**版本**:1.0.0 | ||
| 8 | + | ||
| 9 | +--- | ||
| 10 | + | ||
| 11 | +## 目录 | ||
| 12 | + | ||
| 13 | +1. [架构概览](#架构概览) | ||
| 14 | +2. [核心数据结构](#核心数据结构) | ||
| 15 | +3. [配置生成流程](#配置生成流程) | ||
| 16 | +4. [后端迁移方案](#后端迁移方案) | ||
| 17 | +5. [API 设计建议](#api-设计建议) | ||
| 18 | +6. [数据模型设计](#数据模型设计) | ||
| 19 | + | ||
| 20 | +--- | ||
| 21 | + | ||
| 22 | +## 架构概览 | ||
| 23 | + | ||
| 24 | +### 整体架构图 | ||
| 25 | + | ||
| 26 | +``` | ||
| 27 | +┌─────────────────────────────────────────────────────────────────────────┐ | ||
| 28 | +│ 前端(当前架构) │ | ||
| 29 | +├─────────────────────────────────────────────────────────────────────────┤ | ||
| 30 | +│ │ | ||
| 31 | +│ ┌─────────────────────────────────────────────────────────────────┐ │ | ||
| 32 | +│ │ 配置层 (Config) │ │ | ||
| 33 | +│ │ │ │ | ||
| 34 | +│ │ ┌──────────────────────────────────────────────────────────┐ │ │ | ||
| 35 | +│ │ │ plan-templates.js - 产品模板配置 │ │ │ | ||
| 36 | +│ │ │ ├─ 产品映射: form_sn → TemplateConfig │ │ │ | ||
| 37 | +│ │ │ ├─ 基础 Schema: protectionFormSchema, savingsFormSchema │ │ │ | ||
| 38 | +│ │ │ └─ 字段映射: baseSubmitMapping, savingsSubmitMapping │ │ │ | ||
| 39 | +│ │ └──────────────────────────────────────────────────────────┘ │ │ | ||
| 40 | +│ │ │ │ | ||
| 41 | +│ │ ┌──────────────────────────────────────────────────────────┐ │ │ | ||
| 42 | +│ │ │ plan-fields.js - 字段定义库 │ │ │ | ||
| 43 | +│ │ │ ├─ 字段类型: TEXT, AMOUNT, DATE, RADIO, SELECT... │ │ │ | ||
| 44 | +│ │ │ ├─ 字段分组: BASIC, COVERAGE, WITHDRAWAL │ │ │ | ||
| 45 | +│ │ │ └─ 验证规则: required, min, max, range │ │ │ | ||
| 46 | +│ │ └──────────────────────────────────────────────────────────┘ │ │ | ||
| 47 | +│ └─────────────────────────────────────────────────────────────────┘ │ | ||
| 48 | +│ │ │ | ||
| 49 | +│ ▼ │ | ||
| 50 | +│ ┌─────────────────────────────────────────────────────────────────┐ │ | ||
| 51 | +│ │ 容器层 (Container) │ │ | ||
| 52 | +│ │ │ │ | ||
| 53 | +│ │ ┌──────────────────────────────────────────────────────────┐ │ │ | ||
| 54 | +│ │ │ PlanFormContainer.vue │ │ │ | ||
| 55 | +│ │ │ ├─ 接收产品对象 (product.form_sn) │ │ │ | ||
| 56 | +│ │ │ ├─ 匹配模板配置 (getTemplateConfig) │ │ │ | ||
| 57 | +│ │ │ ├─ 合并后端配置 (product.plan_config) │ │ │ | ||
| 58 | +│ │ │ └─ 动态加载模板组件 │ │ │ | ||
| 59 | +│ │ └──────────────────────────────────────────────────────────┘ │ │ | ||
| 60 | +│ └─────────────────────────────────────────────────────────────────┘ │ | ||
| 61 | +│ │ │ | ||
| 62 | +│ ▼ │ | ||
| 63 | +│ ┌─────────────────────────────────────────────────────────────────┐ │ | ||
| 64 | +│ │ 模板层 (Template) │ │ | ||
| 65 | +│ │ │ │ | ||
| 66 | +│ │ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────┐ │ │ | ||
| 67 | +│ │ │ LifeInsurance │ │ CriticalIllness │ │ Savings │ │ │ | ||
| 68 | +│ │ │ Template.vue │ │ Template.vue │ │ Template.vue │ │ │ | ||
| 69 | +│ │ │ │ │ │ │ │ │ │ | ||
| 70 | +│ │ │ - 基础字段 │ │ - 基础字段 │ │ - 基础字段 │ │ │ | ||
| 71 | +│ │ │ - 保障配置 │ │ - 保障配置 │ │ - 提取计划 │ │ │ | ||
| 72 | +│ │ └──────────────────┘ └──────────────────┘ └──────────────┘ │ │ | ||
| 73 | +│ └─────────────────────────────────────────────────────────────────┘ │ | ||
| 74 | +│ │ │ | ||
| 75 | +│ ▼ │ | ||
| 76 | +│ ┌─────────────────────────────────────────────────────────────────┐ │ | ||
| 77 | +│ │ 字段组件层 (Fields) │ │ | ||
| 78 | +│ │ │ │ | ||
| 79 | +│ │ NameInput │ AmountKeyboard │ DatePicker │ Radio │ AgePicker │ │ | ||
| 80 | +│ └─────────────────────────────────────────────────────────────────┘ │ | ||
| 81 | +│ │ | ||
| 82 | +└─────────────────────────────────────────────────────────────────────────┘ | ||
| 83 | + │ | ||
| 84 | + │ 提交订单 | ||
| 85 | + ▼ | ||
| 86 | +┌─────────────────────────────────────────────────────────────────────────┐ | ||
| 87 | +│ 后端 API │ | ||
| 88 | +│ /srv/?a=proposal&t=add │ | ||
| 89 | +│ - customer_name │ | ||
| 90 | +│ - customer_gender │ | ||
| 91 | +│ - annual_premium (需要 fen→yuan 转换) │ | ||
| 92 | +│ - payment_years │ | ||
| 93 | +│ - withdrawal_option │ | ||
| 94 | +│ ... │ | ||
| 95 | +└─────────────────────────────────────────────────────────────────────────┘ | ||
| 96 | +``` | ||
| 97 | + | ||
| 98 | +### 数据流图 | ||
| 99 | + | ||
| 100 | +``` | ||
| 101 | +产品列表 API | ||
| 102 | + │ | ||
| 103 | + │ { id, product_name, form_sn, plan_config? } | ||
| 104 | + ▼ | ||
| 105 | +┌─────────────────────────────────────────────────────────────┐ | ||
| 106 | +│ PlanFormContainer │ | ||
| 107 | +│ │ | ||
| 108 | +│ 1. 根据 form_sn 从 plan-templates.js 获取配置 │ | ||
| 109 | +│ 2. 合并 product.plan_config (后端覆盖) │ | ||
| 110 | +│ 3. 动态加载对应 Template 组件 │ | ||
| 111 | +└─────────────────────────────────────────────────────────────┘ | ||
| 112 | + │ | ||
| 113 | + │ { config: { currency, payment_periods, form_schema, ... } } | ||
| 114 | + ▼ | ||
| 115 | +┌─────────────────────────────────────────────────────────────┐ | ||
| 116 | +│ Template Component (LifeInsuranceTemplate 等) │ | ||
| 117 | +│ │ | ||
| 118 | +│ 1. 解析 form_schema.base_fields 和 withdrawal_fields │ | ||
| 119 | +│ 2. 根据 field.type 渲染对应的字段组件 │ | ||
| 120 | +│ 3. 使用 useFieldDependencies 管理字段联动 │ | ||
| 121 | +└─────────────────────────────────────────────────────────────┘ | ||
| 122 | + │ | ||
| 123 | + │ 用户填写表单 | ||
| 124 | + │ v-model 双向绑定 | ||
| 125 | + ▼ | ||
| 126 | +┌─────────────────────────────────────────────────────────────┐ | ||
| 127 | +│ 表单数据 formData │ | ||
| 128 | +│ { customer_name, gender, birthday, coverage, ... } │ | ||
| 129 | +└─────────────────────────────────────────────────────────────┘ | ||
| 130 | + │ | ||
| 131 | + │ 点击"生成计划书" | ||
| 132 | + ▼ | ||
| 133 | +┌─────────────────────────────────────────────────────────────┐ | ||
| 134 | +│ submit() 方法 │ | ||
| 135 | +│ │ | ||
| 136 | +│ 1. 调用 template.validate() 校验表单 │ | ||
| 137 | +│ 2. 根据 submit_mapping 映射字段 │ | ||
| 138 | +│ 3. 执行值转换 (fen_to_yuan) │ | ||
| 139 | +│ 4. 调用 addAPI(formData) │ | ||
| 140 | +└─────────────────────────────────────────────────────────────┘ | ||
| 141 | +``` | ||
| 142 | + | ||
| 143 | +--- | ||
| 144 | + | ||
| 145 | +## 核心数据结构 | ||
| 146 | + | ||
| 147 | +### 1. TemplateConfig(模板配置) | ||
| 148 | + | ||
| 149 | +```javascript | ||
| 150 | +// 位置: src/config/plan-templates.js | ||
| 151 | +const PLAN_TEMPLATES = { | ||
| 152 | + 'savings-gs': { // form_sn: 产品唯一标识 | ||
| 153 | + name: '宏挚传承保障计划', // 产品名称 | ||
| 154 | + component: 'SavingsTemplate', // 模板组件名 | ||
| 155 | + category: 'savings', // 产品分类 | ||
| 156 | + | ||
| 157 | + config: { // 模板配置对象 | ||
| 158 | + // === 基础配置 === | ||
| 159 | + currency: 'USD', // 默认币种 | ||
| 160 | + payment_periods: [ // 缴费年期选项 | ||
| 161 | + '整付', '3 年', '5 年', '10 年', '15 年' | ||
| 162 | + ], | ||
| 163 | + age_range: { min: 0, max: 100 }, // 投保年龄范围 | ||
| 164 | + insurance_period: '终身', // 保险期间 | ||
| 165 | + | ||
| 166 | + // === 提取计划配置(储蓄型产品) === | ||
| 167 | + withdrawal_plan: { | ||
| 168 | + enabled: true, // 是否启用提取计划 | ||
| 169 | + currencies: ['HKD', 'USD', 'CNY'], // 支持的币种 | ||
| 170 | + default_currency: 'USD', // 默认币种 | ||
| 171 | + withdrawal_modes: [ // 提取模式 | ||
| 172 | + '指定提取金额', | ||
| 173 | + '最高固定提取金额' | ||
| 174 | + ], | ||
| 175 | + withdrawal_periods: [ // 提取年期 | ||
| 176 | + '1年', '2年', '3年', '5年', | ||
| 177 | + '10年', '15年', '20年', '终身' | ||
| 178 | + ] | ||
| 179 | + }, | ||
| 180 | + | ||
| 181 | + // === 表单 Schema === | ||
| 182 | + form_schema: savingsFormSchema, // 见下方详细说明 | ||
| 183 | + | ||
| 184 | + // === 提交字段映射 === | ||
| 185 | + submit_mapping: savingsSubmitMapping // 见下方详细说明 | ||
| 186 | + } | ||
| 187 | + } | ||
| 188 | +} | ||
| 189 | +``` | ||
| 190 | + | ||
| 191 | +### 2. FormSchema(表单结构定义) | ||
| 192 | + | ||
| 193 | +```javascript | ||
| 194 | +// 位置: src/config/plan-templates.js | ||
| 195 | +const savingsFormSchema = { | ||
| 196 | + // 基础字段:所有产品都有的字段 | ||
| 197 | + base_fields: [ | ||
| 198 | + { | ||
| 199 | + id: 'customer_name', // 唯一标识 | ||
| 200 | + key: 'customer_name', // formData 中的键名 | ||
| 201 | + type: 'name', // 字段类型 | ||
| 202 | + label: '申请人', // 显示标签 | ||
| 203 | + placeholder: '请输入申请人', // 占位符 | ||
| 204 | + required: true // 是否必填 | ||
| 205 | + }, | ||
| 206 | + { | ||
| 207 | + id: 'gender', | ||
| 208 | + key: 'gender', | ||
| 209 | + type: 'radio', // 单选框 | ||
| 210 | + label: '性别', | ||
| 211 | + options: ['男', '女'], // 选项列表 | ||
| 212 | + required: true | ||
| 213 | + }, | ||
| 214 | + { | ||
| 215 | + id: 'birthday', | ||
| 216 | + key: 'birthday', | ||
| 217 | + type: 'date', // 日期选择器 | ||
| 218 | + label: '出生年月日', | ||
| 219 | + placeholder: '请选择年月日', | ||
| 220 | + required: true | ||
| 221 | + }, | ||
| 222 | + { | ||
| 223 | + id: 'coverage', | ||
| 224 | + key: 'coverage', | ||
| 225 | + type: 'amount', // 金额输入(键盘) | ||
| 226 | + label: '年缴保费', | ||
| 227 | + placeholder: '请输入年缴保费', | ||
| 228 | + input_label: '请输入年缴保费金额', | ||
| 229 | + required: true, | ||
| 230 | + currency_from: 'currency' // 币种来源: config.currency | ||
| 231 | + }, | ||
| 232 | + { | ||
| 233 | + id: 'payment_period', | ||
| 234 | + key: 'payment_period', | ||
| 235 | + type: 'payment_period', // 缴费年期(专用组件) | ||
| 236 | + label: '缴费年期', | ||
| 237 | + required: true, | ||
| 238 | + options_from: 'payment_periods' // 选项来源: config.payment_periods | ||
| 239 | + } | ||
| 240 | + ], | ||
| 241 | + | ||
| 242 | + // 提取计划字段:储蓄型产品特有 | ||
| 243 | + withdrawal_fields: [ | ||
| 244 | + { | ||
| 245 | + id: 'withdrawal_enabled', | ||
| 246 | + key: 'withdrawal_enabled', | ||
| 247 | + type: 'radio', | ||
| 248 | + label: '是否希望生成一份允许减少名义金额的提取说明?', | ||
| 249 | + options: ['是', '否'], | ||
| 250 | + required: true, | ||
| 251 | + default: '否' | ||
| 252 | + }, | ||
| 253 | + { | ||
| 254 | + id: 'withdrawal_mode', | ||
| 255 | + key: 'withdrawal_mode', | ||
| 256 | + type: 'radio', | ||
| 257 | + label: '提取选项', | ||
| 258 | + options: ['指定提取金额', '最高固定提取金额'], | ||
| 259 | + required: true, | ||
| 260 | + default: '指定提取金额', | ||
| 261 | + section_title: '款项提取(允许减少名义金额)', | ||
| 262 | + clear_when_hidden: true, // 隐藏时清空值 | ||
| 263 | + | ||
| 264 | + // === 条件显示规则 === | ||
| 265 | + show_when: { | ||
| 266 | + field: 'withdrawal_enabled', // 依赖字段 | ||
| 267 | + op: 'eq', // 操作符: eq/ne/gt/lt/in | ||
| 268 | + value: '是' // 比较值 | ||
| 269 | + } | ||
| 270 | + }, | ||
| 271 | + { | ||
| 272 | + id: 'withdrawal_method', | ||
| 273 | + key: 'withdrawal_method', | ||
| 274 | + type: 'radio', | ||
| 275 | + label: '提取方式', | ||
| 276 | + options: ['按年岁'], | ||
| 277 | + required: true, | ||
| 278 | + default: '按年岁', | ||
| 279 | + show_when: { | ||
| 280 | + field: 'withdrawal_mode', | ||
| 281 | + op: 'eq', | ||
| 282 | + value: '指定提取金额' | ||
| 283 | + }, | ||
| 284 | + clear_when_hidden: true | ||
| 285 | + }, | ||
| 286 | + { | ||
| 287 | + id: 'annual_withdrawal_amount', | ||
| 288 | + key: 'annual_withdrawal_amount', | ||
| 289 | + type: 'amount', | ||
| 290 | + label: '每年提取金额', | ||
| 291 | + placeholder: '请输入每年提取金额', | ||
| 292 | + input_label: '请输入每年提取金额', | ||
| 293 | + required: true, | ||
| 294 | + currency_from: 'withdrawal_plan.default_currency', | ||
| 295 | + show_when: { | ||
| 296 | + field: 'withdrawal_mode', | ||
| 297 | + op: 'eq', | ||
| 298 | + value: '指定提取金额' | ||
| 299 | + }, | ||
| 300 | + clear_when_hidden: true | ||
| 301 | + }, | ||
| 302 | + // ... 更多字段 | ||
| 303 | + ] | ||
| 304 | +} | ||
| 305 | +``` | ||
| 306 | + | ||
| 307 | +### 3. SubmitMapping(提交字段映射) | ||
| 308 | + | ||
| 309 | +```javascript | ||
| 310 | +// 位置: src/config/plan-templates.js | ||
| 311 | +const savingsSubmitMapping = { | ||
| 312 | + // 基础字段映射 | ||
| 313 | + customer_name: { | ||
| 314 | + api_field: 'customer_name' // API 字段名 | ||
| 315 | + }, | ||
| 316 | + gender: { | ||
| 317 | + api_field: 'customer_gender' | ||
| 318 | + }, | ||
| 319 | + birthday: { | ||
| 320 | + api_field: 'customer_birthday' | ||
| 321 | + }, | ||
| 322 | + smoker: { | ||
| 323 | + api_field: 'smoking_status' | ||
| 324 | + }, | ||
| 325 | + coverage: { | ||
| 326 | + api_field: 'annual_premium', // API 字段名 | ||
| 327 | + transform: 'fen_to_yuan' // 值转换: 分→元 | ||
| 328 | + }, | ||
| 329 | + payment_period: { | ||
| 330 | + api_field: 'payment_years' | ||
| 331 | + }, | ||
| 332 | + | ||
| 333 | + // 提取计划字段映射 | ||
| 334 | + withdrawal_enabled: { | ||
| 335 | + api_field: 'allow_reduce_amount' | ||
| 336 | + }, | ||
| 337 | + withdrawal_mode: { | ||
| 338 | + api_field: 'withdrawal_option' | ||
| 339 | + }, | ||
| 340 | + annual_withdrawal_amount: { | ||
| 341 | + api_field: 'annual_withdrawal_amount', | ||
| 342 | + transform: 'fen_to_yuan' | ||
| 343 | + }, | ||
| 344 | + // 指定提取金额模式的特殊字段 | ||
| 345 | + withdrawal_start_age_specified: { | ||
| 346 | + api_field: 'withdrawal_start_age' // 根据模式动态选择 | ||
| 347 | + }, | ||
| 348 | + withdrawal_period_specified: { | ||
| 349 | + api_field: 'withdrawal_period' | ||
| 350 | + }, | ||
| 351 | + // 最高固定提取金额模式的特殊字段 | ||
| 352 | + withdrawal_start_age_fixed: { | ||
| 353 | + api_field: 'withdrawal_start_age' | ||
| 354 | + }, | ||
| 355 | + withdrawal_period_fixed: { | ||
| 356 | + api_field: 'withdrawal_period' | ||
| 357 | + } | ||
| 358 | +} | ||
| 359 | +``` | ||
| 360 | + | ||
| 361 | +### 4. FieldDefinitions(字段定义) | ||
| 362 | + | ||
| 363 | +```javascript | ||
| 364 | +// 位置: src/config/plan-fields.js | ||
| 365 | +export const PLAN_FIELD_DEFINITIONS = { | ||
| 366 | + customer_name: { | ||
| 367 | + label: '申请人', | ||
| 368 | + type: FIELD_TYPES.TEXT, | ||
| 369 | + required: true, | ||
| 370 | + api_field: 'customer_name', | ||
| 371 | + component: 'PlanFieldName', | ||
| 372 | + group: FIELD_GROUPS.BASIC, | ||
| 373 | + validation: { | ||
| 374 | + required: (value) => value?.trim()?.length >= 2 | ||
| 375 | + } | ||
| 376 | + }, | ||
| 377 | + coverage: { | ||
| 378 | + label: '保额', | ||
| 379 | + type: FIELD_TYPES.AMOUNT, | ||
| 380 | + required: true, | ||
| 381 | + api_field: 'annual_premium', | ||
| 382 | + transform: TRANSFORM_TYPES.FEN_TO_YUAN, | ||
| 383 | + component: 'PlanFieldAmount', | ||
| 384 | + group: FIELD_GROUPS.COVERAGE, | ||
| 385 | + placeholder: '请输入保额', | ||
| 386 | + validation: { | ||
| 387 | + required: (value) => value > 0, | ||
| 388 | + min: (value, config) => value >= (config?.min_coverage || 1000), | ||
| 389 | + max: (value, config) => value <= (config?.max_coverage || 10000000) | ||
| 390 | + } | ||
| 391 | + }, | ||
| 392 | + // ... 更多字段定义 | ||
| 393 | +} | ||
| 394 | +``` | ||
| 395 | + | ||
| 396 | +--- | ||
| 397 | + | ||
| 398 | +## 配置生成流程 | ||
| 399 | + | ||
| 400 | +### 1. 文档解析生成配置(当前工具) | ||
| 401 | + | ||
| 402 | +``` | ||
| 403 | +┌─────────────────────────────────────────────────────────────────────────┐ | ||
| 404 | +│ 文档解析流程 │ | ||
| 405 | +│ │ | ||
| 406 | +│ 产品文档 (PDF/Word) │ | ||
| 407 | +│ │ │ | ||
| 408 | +│ ▼ │ | ||
| 409 | +│ ┌───────────────────────────────────────────────────────────────┐ │ | ||
| 410 | +│ │ AI 文档解析器 (/admin/document-parser/index) │ │ | ||
| 411 | +│ │ ├─ 豆包 AI / OpenAI API │ │ | ||
| 412 | +│ │ ├─ 智能字段提取 │ │ | ||
| 413 | +│ │ └─ 生成结构化配置 │ │ | ||
| 414 | +│ └───────────────────────────────────────────────────────────────┘ │ | ||
| 415 | +│ │ │ | ||
| 416 | +│ ▼ │ | ||
| 417 | +│ ┌───────────────────────────────────────────────────────────────┐ │ | ||
| 418 | +│ │ 提取的配置数据 │ │ | ||
| 419 | +│ │ { │ │ | ||
| 420 | +│ │ product_name: "宏挚传承保障计划", │ │ | ||
| 421 | +│ │ product_type: "savings", │ │ | ||
| 422 | +│ │ currency: "USD", │ │ | ||
| 423 | +│ │ payment_periods: ["整付", "3 年", ...], │ │ | ||
| 424 | +│ │ form_schema: { base_fields: [...], withdrawal_fields: [...] }│ │ | ||
| 425 | +│ │ } │ │ | ||
| 426 | +│ └───────────────────────────────────────────────────────────────┘ │ | ||
| 427 | +│ │ │ | ||
| 428 | +│ ▼ │ | ||
| 429 | +│ ┌───────────────────────────────────────────────────────────────┐ │ | ||
| 430 | +│ │ config-generator.js - 配置代码生成器 │ │ | ||
| 431 | +│ │ ├─ generateConfigCode() - 生成配置代码 │ │ | ||
| 432 | +│ │ ├─ generateSavingsConfig() - 生成储蓄型配置 │ │ | ||
| 433 | +│ │ └─ buildSchemaCode() - 构建 Schema 代码 │ │ | ||
| 434 | +│ └───────────────────────────────────────────────────────────────┘ │ | ||
| 435 | +│ │ │ | ||
| 436 | +│ ▼ │ | ||
| 437 | +│ 配置代码(复制到 plan-templates.js) │ | ||
| 438 | +└─────────────────────────────────────────────────────────────────────────┘ | ||
| 439 | +``` | ||
| 440 | + | ||
| 441 | +### 2. 运行时配置加载流程 | ||
| 442 | + | ||
| 443 | +```javascript | ||
| 444 | +// PlanFormContainer.vue 中的关键代码 | ||
| 445 | + | ||
| 446 | +// 1. 根据 form_sn 获取模板配置 | ||
| 447 | +const templateConfig = computed(() => { | ||
| 448 | + const config = PLAN_TEMPLATES[props.product.form_sn] | ||
| 449 | + | ||
| 450 | + // 2. 合并后端返回的 plan_config(覆盖默认配置) | ||
| 451 | + return { | ||
| 452 | + ...config.config, // 默认配置 | ||
| 453 | + ...config, // 顶层属性(name, component 等) | ||
| 454 | + ...(props.product.plan_config || {}) // 后端动态配置 | ||
| 455 | + } | ||
| 456 | +}) | ||
| 457 | + | ||
| 458 | +// 3. 动态加载模板组件 | ||
| 459 | +const currentTemplateComponent = computed(() => { | ||
| 460 | + const componentMap = { | ||
| 461 | + 'LifeInsuranceTemplate': LifeInsuranceTemplate, | ||
| 462 | + 'CriticalIllnessTemplate': CriticalIllnessTemplate, | ||
| 463 | + 'SavingsTemplate': SavingsTemplate | ||
| 464 | + } | ||
| 465 | + return componentMap[templateConfig.value.component] | ||
| 466 | +}) | ||
| 467 | +``` | ||
| 468 | + | ||
| 469 | +--- | ||
| 470 | + | ||
| 471 | +## 后端迁移方案 | ||
| 472 | + | ||
| 473 | +### 方案对比 | ||
| 474 | + | ||
| 475 | +| 方案 | 优势 | 劣势 | 推荐度 | | ||
| 476 | +|------|------|------|--------| | ||
| 477 | +| **方案 A:完全后端化** | 前端代码最少,配置集中管理 | 后端复杂度高,需要维护表单渲染引擎 | ⭐⭐⭐ | | ||
| 478 | +| **方案 B:配置 API 化** | 前端保留灵活性,后端只管理配置 | 前端仍需维护模板组件 | ⭐⭐⭐⭐⭐ | | ||
| 479 | +| **方案 C:混合模式** | 平衡前后端职责,渐进式迁移 | 架构稍复杂 | ⭐⭐⭐⭐ | | ||
| 480 | + | ||
| 481 | +### 推荐方案:方案 B(配置 API 化) | ||
| 482 | + | ||
| 483 | +``` | ||
| 484 | +┌─────────────────────────────────────────────────────────────────────────┐ | ||
| 485 | +│ 迁移后架构(方案 B) │ | ||
| 486 | +├─────────────────────────────────────────────────────────────────────────┤ | ||
| 487 | +│ │ | ||
| 488 | +│ ┌─────────────────────────────────────────────────────────────────┐ │ | ||
| 489 | +│ │ 后端配置管理 │ │ | ||
| 490 | +│ │ │ │ | ||
| 491 | +│ │ 产品配置表 (product_configs) │ │ | ||
| 492 | +│ │ ├─ form_sn │ │ | ||
| 493 | +│ │ ├─ form_schema (JSON) │ │ | ||
| 494 | +│ │ ├─ submit_mapping (JSON) │ │ | ||
| 495 | +│ │ └─ metadata (currency, age_range, ...) │ │ | ||
| 496 | +│ └─────────────────────────────────────────────────────────────────┘ │ | ||
| 497 | +│ │ │ | ||
| 498 | +│ │ GET /api/plan/config/:form_sn │ | ||
| 499 | +│ ▼ │ | ||
| 500 | +│ ┌─────────────────────────────────────────────────────────────────┐ │ | ||
| 501 | +│ │ 前端配置缓存 │ │ | ||
| 502 | +│ │ │ │ | ||
| 503 | +│ │ Pinia Store (planConfigStore) │ │ | ||
| 504 | +│ │ ├─ configs: Map<form_sn, TemplateConfig> │ │ | ||
| 505 | +│ │ ├─ fetchConfig(formSn) - 拉取配置 │ │ | ||
| 506 | +│ │ └─ prefetchAll() - 预加载所有配置 │ │ | ||
| 507 | +│ └─────────────────────────────────────────────────────────────────┘ │ | ||
| 508 | +│ │ │ | ||
| 509 | +│ ▼ │ | ||
| 510 | +│ ┌─────────────────────────────────────────────────────────────────┐ │ | ||
| 511 | +│ │ PlanFormContainer │ │ | ||
| 512 | +│ │ (逻辑不变,只是配置来源变了) │ │ | ||
| 513 | +│ └─────────────────────────────────────────────────────────────────┘ │ | ||
| 514 | +│ │ │ | ||
| 515 | +│ ▼ │ | ||
| 516 | +│ ┌─────────────────────────────────────────────────────────────────┐ │ | ||
| 517 | +│ │ Template Components │ │ | ||
| 518 | +│ │ (保持不变) │ │ | ||
| 519 | +│ └─────────────────────────────────────────────────────────────────┘ │ | ||
| 520 | +│ │ | ||
| 521 | +└─────────────────────────────────────────────────────────────────────────┘ | ||
| 522 | +``` | ||
| 523 | + | ||
| 524 | +--- | ||
| 525 | + | ||
| 526 | +## API 设计建议 | ||
| 527 | + | ||
| 528 | +### 1. 获取计划书配置 | ||
| 529 | + | ||
| 530 | +```http | ||
| 531 | +GET /api/plan/config/:form_sn | ||
| 532 | +``` | ||
| 533 | + | ||
| 534 | +**Response**: | ||
| 535 | +```json | ||
| 536 | +{ | ||
| 537 | + "code": 1, | ||
| 538 | + "data": { | ||
| 539 | + "form_sn": "savings-gs", | ||
| 540 | + "name": "宏挚传承保障计划", | ||
| 541 | + "component": "SavingsTemplate", | ||
| 542 | + "config": { | ||
| 543 | + "currency": "USD", | ||
| 544 | + "payment_periods": ["整付", "3 年", "5 年"], | ||
| 545 | + "age_range": { "min": 0, "max": 100 }, | ||
| 546 | + "insurance_period": "终身", | ||
| 547 | + "withdrawal_plan": { | ||
| 548 | + "enabled": true, | ||
| 549 | + "currencies": ["HKD", "USD", "CNY"], | ||
| 550 | + "default_currency": "USD", | ||
| 551 | + "withdrawal_modes": ["指定提取金额", "最高固定提取金额"], | ||
| 552 | + "withdrawal_periods": ["1年", "2年", "3年"] | ||
| 553 | + }, | ||
| 554 | + "form_schema": { | ||
| 555 | + "base_fields": [ | ||
| 556 | + { | ||
| 557 | + "id": "customer_name", | ||
| 558 | + "key": "customer_name", | ||
| 559 | + "type": "name", | ||
| 560 | + "label": "申请人", | ||
| 561 | + "placeholder": "请输入申请人", | ||
| 562 | + "required": true | ||
| 563 | + } | ||
| 564 | + // ... 更多字段 | ||
| 565 | + ], | ||
| 566 | + "withdrawal_fields": [ | ||
| 567 | + // ... 提取计划字段 | ||
| 568 | + ] | ||
| 569 | + }, | ||
| 570 | + "submit_mapping": { | ||
| 571 | + "customer_name": { "api_field": "customer_name" }, | ||
| 572 | + "coverage": { "api_field": "annual_premium", "transform": "fen_to_yuan" } | ||
| 573 | + // ... 更多映射 | ||
| 574 | + } | ||
| 575 | + } | ||
| 576 | + } | ||
| 577 | +} | ||
| 578 | +``` | ||
| 579 | + | ||
| 580 | +### 2. 批量获取配置(预加载) | ||
| 581 | + | ||
| 582 | +```http | ||
| 583 | +GET /api/plan/config/batch?form_sn[]=savings-gs&form_sn[]=life-insurance-wiop3e | ||
| 584 | +``` | ||
| 585 | + | ||
| 586 | +**Response**: | ||
| 587 | +```json | ||
| 588 | +{ | ||
| 589 | + "code": 1, | ||
| 590 | + "data": { | ||
| 591 | + "savings-gs": { /* 配置对象 */ }, | ||
| 592 | + "life-insurance-wiop3e": { /* 配置对象 */ } | ||
| 593 | + } | ||
| 594 | +} | ||
| 595 | +``` | ||
| 596 | + | ||
| 597 | +### 3. 创建计划书(保持不变) | ||
| 598 | + | ||
| 599 | +```http | ||
| 600 | +POST /srv/?a=proposal&t=add | ||
| 601 | +``` | ||
| 602 | + | ||
| 603 | +**Request**: | ||
| 604 | +```json | ||
| 605 | +{ | ||
| 606 | + "product_id": 123, | ||
| 607 | + "customer_name": "张三", | ||
| 608 | + "customer_gender": "male", | ||
| 609 | + "annual_premium": 100000, // 单位:元 | ||
| 610 | + "payment_years": "5 年", | ||
| 611 | + "withdrawal_option": "指定提取金额", | ||
| 612 | + "allow_reduce_amount": true | ||
| 613 | + // ... 其他字段 | ||
| 614 | +} | ||
| 615 | +``` | ||
| 616 | + | ||
| 617 | +--- | ||
| 618 | + | ||
| 619 | +## 数据模型设计 | ||
| 620 | + | ||
| 621 | +### product_configs 表 | ||
| 622 | + | ||
| 623 | +```sql | ||
| 624 | +CREATE TABLE product_configs ( | ||
| 625 | + id BIGINT PRIMARY KEY AUTO_INCREMENT, | ||
| 626 | + form_sn VARCHAR(100) UNIQUE NOT NULL COMMENT '产品表单标识', | ||
| 627 | + name VARCHAR(200) NOT NULL COMMENT '产品名称', | ||
| 628 | + component VARCHAR(50) NOT NULL COMMENT '模板组件名', | ||
| 629 | + category VARCHAR(50) COMMENT '产品分类', | ||
| 630 | + | ||
| 631 | + -- 基础配置 (JSON) | ||
| 632 | + config JSON NOT NULL COMMENT '模板配置', | ||
| 633 | + | ||
| 634 | + -- 元数据 (冗余字段,方便查询) | ||
| 635 | + currency VARCHAR(10) COMMENT '默认币种', | ||
| 636 | + age_min INT COMMENT '最小投保年龄', | ||
| 637 | + age_max INT COMMENT '最大投保年龄', | ||
| 638 | + insurance_period VARCHAR(50) COMMENT '保险期间', | ||
| 639 | + | ||
| 640 | + -- 时间戳 | ||
| 641 | + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, | ||
| 642 | + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, | ||
| 643 | + | ||
| 644 | + INDEX idx_form_sn (form_sn), | ||
| 645 | + INDEX idx_category (category) | ||
| 646 | +) COMMENT='计划书产品配置表'; | ||
| 647 | +``` | ||
| 648 | + | ||
| 649 | +### config JSON 结构 | ||
| 650 | + | ||
| 651 | +```json | ||
| 652 | +{ | ||
| 653 | + "currency": "USD", | ||
| 654 | + "payment_periods": ["整付", "3 年", "5 年"], | ||
| 655 | + "age_range": { "min": 0, "max": 100 }, | ||
| 656 | + "insurance_period": "终身", | ||
| 657 | + "withdrawal_plan": { | ||
| 658 | + "enabled": true, | ||
| 659 | + "currencies": ["HKD", "USD", "CNY"], | ||
| 660 | + "default_currency": "USD", | ||
| 661 | + "withdrawal_modes": ["指定提取金额", "最高固定提取金额"], | ||
| 662 | + "withdrawal_periods": ["1年", "2年", "3年"] | ||
| 663 | + }, | ||
| 664 | + "form_schema": { | ||
| 665 | + "base_fields": [ | ||
| 666 | + { | ||
| 667 | + "id": "customer_name", | ||
| 668 | + "key": "customer_name", | ||
| 669 | + "type": "name", | ||
| 670 | + "label": "申请人", | ||
| 671 | + "placeholder": "请输入申请人", | ||
| 672 | + "required": true | ||
| 673 | + } | ||
| 674 | + ], | ||
| 675 | + "withdrawal_fields": [] | ||
| 676 | + }, | ||
| 677 | + "submit_mapping": { | ||
| 678 | + "customer_name": { "api_field": "customer_name" }, | ||
| 679 | + "coverage": { "api_field": "annual_premium", "transform": "fen_to_yuan" } | ||
| 680 | + } | ||
| 681 | +} | ||
| 682 | +``` | ||
| 683 | + | ||
| 684 | +--- | ||
| 685 | + | ||
| 686 | +## 迁移步骤建议 | ||
| 687 | + | ||
| 688 | +### 阶段 1:后端配置存储(1-2 周) | ||
| 689 | + | ||
| 690 | +- [ ] 创建 `product_configs` 表 | ||
| 691 | +- [ ] 迁移现有配置到数据库 | ||
| 692 | +- [ ] 实现配置 CRUD API | ||
| 693 | + | ||
| 694 | +### 阶段 2:前端配置 API 集成(1 周) | ||
| 695 | + | ||
| 696 | +- [ ] 创建 `planConfigStore` | ||
| 697 | +- [ ] 修改 `PlanFormContainer` 使用 API 配置 | ||
| 698 | +- [ ] 添加配置缓存和预加载逻辑 | ||
| 699 | + | ||
| 700 | +### 阶段 3:配置管理后台(1-2 周) | ||
| 701 | + | ||
| 702 | +- [ ] 开发配置管理界面 | ||
| 703 | +- [ ] 支持可视化配置表单字段 | ||
| 704 | +- [ ] 支持配置预览和测试 | ||
| 705 | + | ||
| 706 | +### 阶段 4:文档解析集成(1 周) | ||
| 707 | + | ||
| 708 | +- [ ] 将文档解析器连接到配置 API | ||
| 709 | +- [ ] 支持自动创建和更新配置 | ||
| 710 | + | ||
| 711 | +--- | ||
| 712 | + | ||
| 713 | +## 附录 | ||
| 714 | + | ||
| 715 | +### A. 字段类型与组件映射表 | ||
| 716 | + | ||
| 717 | +| field.type | 组件 | 说明 | | ||
| 718 | +|------------|------|------| | ||
| 719 | +| `name` | PlanFieldName | 姓名输入(特殊验证) | | ||
| 720 | +| `text` | nut-input | 普通文本输入 | | ||
| 721 | +| `amount` | PlanFieldAmount | 金额键盘输入 | | ||
| 722 | +| `percentage` | nut-input | 百分比输入(0-100) | | ||
| 723 | +| `date` | PlanFieldDatePicker | 日期选择器 | | ||
| 724 | +| `age` | PlanFieldAgePicker | 年龄选择器 | | ||
| 725 | +| `radio` | PlanFieldRadio | 单选按钮组 | | ||
| 726 | +| `select` | PlanFieldSelect | 下拉选择器 | | ||
| 727 | +| `payment_period` | PaymentPeriodRadio | 缴费年期(专用) | | ||
| 728 | + | ||
| 729 | +### B. 条件操作符 | ||
| 730 | + | ||
| 731 | +| 操作符 | 说明 | 示例 | | ||
| 732 | +|--------|------|------| | ||
| 733 | +| `eq` | 等于 | `{ field: 'gender', op: 'eq', value: 'male' }` | | ||
| 734 | +| `ne` | 不等于 | `{ field: 'gender', op: 'ne', value: 'male' }` | | ||
| 735 | +| `gt` | 大于 | `{ field: 'age', op: 'gt', value: 18 }` | | ||
| 736 | +| `lt` | 小于 | `{ field: 'age', op: 'lt', value: 65 }` | | ||
| 737 | +| `in` | 包含于 | `{ field: 'type', op: 'in', value: ['A', 'B'] }` | | ||
| 738 | + | ||
| 739 | +### C. 值转换类型 | ||
| 740 | + | ||
| 741 | +| transform | 说明 | 方向 | | ||
| 742 | +|-----------|------|------| | ||
| 743 | +| `fen_to_yuan` | 分 → 元 | 提交时 | | ||
| 744 | +| `yuan_to_fen` | 元 → 分 | 回显时 | | ||
| 745 | +| `none` | 不转换 | - | | ||
| 746 | + | ||
| 747 | +--- | ||
| 748 | + | ||
| 749 | +## 相关文档 | ||
| 750 | + | ||
| 751 | +- [计划书录入架构设计](./plan-entry-architecture.md) | ||
| 752 | +- [表单 Schema 使用指南](./plan-form-schema-usage.md) | ||
| 753 | +- [快速上手指南](./plan-entry-quick-guide.md) | ||
| 754 | +- [字段依赖管理](../composables/plan-field-dependencies.md) |
docs/PLAN/plan-config-schema-reference.md
0 → 100644
| 1 | +# 计划书配置 JSON Schema 规范 | ||
| 2 | + | ||
| 3 | +> **文档目的**:定义计划书配置的完整 JSON Schema,用于后端验证和前端类型检查 | ||
| 4 | + | ||
| 5 | +**作者**:Claude Code | ||
| 6 | +**创建日期**:2026-02-25 | ||
| 7 | +**版本**:1.0.0 | ||
| 8 | + | ||
| 9 | +--- | ||
| 10 | + | ||
| 11 | +## 目录 | ||
| 12 | + | ||
| 13 | +1. [配置根对象](#配置根对象) | ||
| 14 | +2. [FormSchema 规范](#formschema-规范) | ||
| 15 | +3. [SubmitMapping 规范](#submitmapping-规范) | ||
| 16 | +4. [字段定义规范](#字段定义规范) | ||
| 17 | +5. [条件规则规范](#条件规则规范) | ||
| 18 | + | ||
| 19 | +--- | ||
| 20 | + | ||
| 21 | +## 配置根对象 | ||
| 22 | + | ||
| 23 | +### TemplateConfig | ||
| 24 | + | ||
| 25 | +```json | ||
| 26 | +{ | ||
| 27 | + "$schema": "http://json-schema.org/draft-07/schema#", | ||
| 28 | + "title": "计划书模板配置", | ||
| 29 | + "type": "object", | ||
| 30 | + "required": ["form_sn", "name", "component", "config"], | ||
| 31 | + "properties": { | ||
| 32 | + "form_sn": { | ||
| 33 | + "type": "string", | ||
| 34 | + "description": "产品唯一标识,格式: {category}-{product_code}", | ||
| 35 | + "pattern": "^(life-insurance|critical-illness|savings)-[a-z0-9-]+$", | ||
| 36 | + "examples": ["savings-gs", "life-insurance-wiop3e"] | ||
| 37 | + }, | ||
| 38 | + "name": { | ||
| 39 | + "type": "string", | ||
| 40 | + "description": "产品名称", | ||
| 41 | + "examples": ["宏挚传承保障计划", "WIOP3E 盈传创富保障计划 3"] | ||
| 42 | + }, | ||
| 43 | + "component": { | ||
| 44 | + "type": "string", | ||
| 45 | + "enum": ["LifeInsuranceTemplate", "CriticalIllnessTemplate", "SavingsTemplate"], | ||
| 46 | + "description": "对应的模板组件名" | ||
| 47 | + }, | ||
| 48 | + "category": { | ||
| 49 | + "type": "string", | ||
| 50 | + "enum": ["life-insurance", "critical-illness", "savings"], | ||
| 51 | + "description": "产品分类" | ||
| 52 | + }, | ||
| 53 | + "config": { | ||
| 54 | + "$ref": "#/definitions/TemplateConfigData" | ||
| 55 | + } | ||
| 56 | + }, | ||
| 57 | + "definitions": { | ||
| 58 | + "TemplateConfigData": { | ||
| 59 | + "type": "object", | ||
| 60 | + "required": ["currency", "payment_periods", "age_range", "form_schema"], | ||
| 61 | + "properties": { | ||
| 62 | + "currency": { | ||
| 63 | + "$ref": "#/definitions/CurrencyCode" | ||
| 64 | + }, | ||
| 65 | + "payment_periods": { | ||
| 66 | + "type": "array", | ||
| 67 | + "items": { "type": "string" }, | ||
| 68 | + "description": "缴费年期选项", | ||
| 69 | + "examples": [["整付", "5 年", "10 年"]] | ||
| 70 | + }, | ||
| 71 | + "age_range": { | ||
| 72 | + "$ref": "#/definitions/AgeRange" | ||
| 73 | + }, | ||
| 74 | + "insurance_period": { | ||
| 75 | + "type": "string", | ||
| 76 | + "description": "保险期间", | ||
| 77 | + "examples": ["终身", "至 100 岁"] | ||
| 78 | + }, | ||
| 79 | + "withdrawal_plan": { | ||
| 80 | + "$ref": "#/definitions/WithdrawalPlan" | ||
| 81 | + }, | ||
| 82 | + "form_schema": { | ||
| 83 | + "$ref": "#/definitions/FormSchema" | ||
| 84 | + }, | ||
| 85 | + "submit_mapping": { | ||
| 86 | + "$ref": "#/definitions/SubmitMapping" | ||
| 87 | + } | ||
| 88 | + } | ||
| 89 | + } | ||
| 90 | + } | ||
| 91 | +} | ||
| 92 | +``` | ||
| 93 | + | ||
| 94 | +### 辅助定义 | ||
| 95 | + | ||
| 96 | +```json | ||
| 97 | +{ | ||
| 98 | + "definitions": { | ||
| 99 | + "CurrencyCode": { | ||
| 100 | + "type": "string", | ||
| 101 | + "enum": ["CNY", "USD", "HKD", "EUR"], | ||
| 102 | + "description": "币种代码" | ||
| 103 | + }, | ||
| 104 | + "AgeRange": { | ||
| 105 | + "type": "object", | ||
| 106 | + "properties": { | ||
| 107 | + "min": { | ||
| 108 | + "type": "integer", | ||
| 109 | + "minimum": 0, | ||
| 110 | + "description": "最小年龄" | ||
| 111 | + }, | ||
| 112 | + "max": { | ||
| 113 | + "type": "integer", | ||
| 114 | + "maximum": 150, | ||
| 115 | + "description": "最大年龄" | ||
| 116 | + } | ||
| 117 | + }, | ||
| 118 | + "required": ["min", "max"] | ||
| 119 | + } | ||
| 120 | + } | ||
| 121 | +} | ||
| 122 | +``` | ||
| 123 | + | ||
| 124 | +--- | ||
| 125 | + | ||
| 126 | +## FormSchema 规范 | ||
| 127 | + | ||
| 128 | +### FormSchema 根对象 | ||
| 129 | + | ||
| 130 | +```json | ||
| 131 | +{ | ||
| 132 | + "FormSchema": { | ||
| 133 | + "type": "object", | ||
| 134 | + "properties": { | ||
| 135 | + "base_fields": { | ||
| 136 | + "type": "array", | ||
| 137 | + "items": { "$ref": "#/definitions/FieldDefinition" }, | ||
| 138 | + "description": "基础字段(所有产品共有)" | ||
| 139 | + }, | ||
| 140 | + "withdrawal_fields": { | ||
| 141 | + "type": "array", | ||
| 142 | + "items": { "$ref": "#/definitions/FieldDefinition" }, | ||
| 143 | + "description": "提取计划字段(储蓄型产品特有)" | ||
| 144 | + } | ||
| 145 | + }, | ||
| 146 | + "required": ["base_fields"] | ||
| 147 | + } | ||
| 148 | +} | ||
| 149 | +``` | ||
| 150 | + | ||
| 151 | +--- | ||
| 152 | + | ||
| 153 | +## 字段定义规范 | ||
| 154 | + | ||
| 155 | +### FieldDefinition | ||
| 156 | + | ||
| 157 | +```json | ||
| 158 | +{ | ||
| 159 | + "FieldDefinition": { | ||
| 160 | + "type": "object", | ||
| 161 | + "required": ["id", "key", "type", "label"], | ||
| 162 | + "properties": { | ||
| 163 | + "id": { | ||
| 164 | + "type": "string", | ||
| 165 | + "description": "字段唯一标识(用于 v-for key)" | ||
| 166 | + }, | ||
| 167 | + "key": { | ||
| 168 | + "type": "string", | ||
| 169 | + "description": "formData 中的键名" | ||
| 170 | + }, | ||
| 171 | + "type": { | ||
| 172 | + "$ref": "#/definitions/FieldType", | ||
| 173 | + "description": "字段类型(决定使用哪个组件)" | ||
| 174 | + }, | ||
| 175 | + "label": { | ||
| 176 | + "type": "string", | ||
| 177 | + "description": "字段显示标签" | ||
| 178 | + }, | ||
| 179 | + "placeholder": { | ||
| 180 | + "type": "string", | ||
| 181 | + "description": "占位符文本" | ||
| 182 | + }, | ||
| 183 | + "input_label": { | ||
| 184 | + "type": "string", | ||
| 185 | + "description": "金额键盘弹窗标题(amount 类型专用)" | ||
| 186 | + }, | ||
| 187 | + "required": { | ||
| 188 | + "type": "boolean", | ||
| 189 | + "description": "是否必填", | ||
| 190 | + "default": false | ||
| 191 | + }, | ||
| 192 | + "default": { | ||
| 193 | + "description": "默认值", | ||
| 194 | + "oneOf": [ | ||
| 195 | + { "type": "string" }, | ||
| 196 | + { "type": "boolean" }, | ||
| 197 | + { "type": "number" } | ||
| 198 | + ] | ||
| 199 | + }, | ||
| 200 | + "options": { | ||
| 201 | + "type": "array", | ||
| 202 | + "items": { "type": "string" }, | ||
| 203 | + "description": "选项列表(radio/select 类型)" | ||
| 204 | + }, | ||
| 205 | + "options_from": { | ||
| 206 | + "type": "string", | ||
| 207 | + "description": "选项来源配置引用", | ||
| 208 | + "examples": ["payment_periods", "withdrawal_plan.withdrawal_periods"] | ||
| 209 | + }, | ||
| 210 | + "currency_from": { | ||
| 211 | + "type": "string", | ||
| 212 | + "description": "币种来源配置引用", | ||
| 213 | + "examples": ["currency", "withdrawal_plan.default_currency"] | ||
| 214 | + }, | ||
| 215 | + "section_title": { | ||
| 216 | + "type": "string", | ||
| 217 | + "description": "分组标题(用于字段分组显示)" | ||
| 218 | + }, | ||
| 219 | + "show_when": { | ||
| 220 | + "$ref": "#/definitions/ConditionRule", | ||
| 221 | + "description": "条件显示规则" | ||
| 222 | + }, | ||
| 223 | + "clear_when_hidden": { | ||
| 224 | + "description": "隐藏时是否清空值", | ||
| 225 | + "oneOf": [ | ||
| 226 | + { "type": "boolean" }, | ||
| 227 | + { "type": "null" }, | ||
| 228 | + { | ||
| 229 | + "type": "object", | ||
| 230 | + "properties": { | ||
| 231 | + "clear_self": { "type": "boolean" }, | ||
| 232 | + "clear_dependents": { | ||
| 233 | + "type": "array", | ||
| 234 | + "items": { "type": "string" } | ||
| 235 | + } | ||
| 236 | + } | ||
| 237 | + } | ||
| 238 | + ] | ||
| 239 | + } | ||
| 240 | + } | ||
| 241 | + } | ||
| 242 | +} | ||
| 243 | +``` | ||
| 244 | + | ||
| 245 | +### FieldType 枚举 | ||
| 246 | + | ||
| 247 | +```json | ||
| 248 | +{ | ||
| 249 | + "FieldType": { | ||
| 250 | + "type": "string", | ||
| 251 | + "enum": [ | ||
| 252 | + "name", | ||
| 253 | + "text", | ||
| 254 | + "amount", | ||
| 255 | + "percentage", | ||
| 256 | + "date", | ||
| 257 | + "age", | ||
| 258 | + "radio", | ||
| 259 | + "select", | ||
| 260 | + "payment_period" | ||
| 261 | + ], | ||
| 262 | + "description": "字段类型枚举" | ||
| 263 | + } | ||
| 264 | +} | ||
| 265 | +``` | ||
| 266 | + | ||
| 267 | +### 字段类型与组件映射 | ||
| 268 | + | ||
| 269 | +| type | 组件 | 说明 | options_from 支持 | | ||
| 270 | +|------|------|------|-------------------| | ||
| 271 | +| `name` | PlanFieldName | 姓名输入 | ❌ | | ||
| 272 | +| `text` | nut-input | 普通文本 | ❌ | | ||
| 273 | +| `amount` | PlanFieldAmount | 金额键盘 | ✅ currency_from | | ||
| 274 | +| `percentage` | nut-input | 百分比 (0-100) | ❌ | | ||
| 275 | +| `date` | PlanFieldDatePicker | 日期选择器 | ❌ | | ||
| 276 | +| `age` | PlanFieldAgePicker | 年龄选择器 | ❌ | | ||
| 277 | +| `radio` | PlanFieldRadio | 单选按钮 | ✅ options_from | | ||
| 278 | +| `select` | PlanFieldSelect | 下拉选择器 | ✅ options_from | | ||
| 279 | +| `payment_period` | PaymentPeriodRadio | 缴费年期专用 | ✅ options_from | | ||
| 280 | + | ||
| 281 | +--- | ||
| 282 | + | ||
| 283 | +## 条件规则规范 | ||
| 284 | + | ||
| 285 | +### ConditionRule | ||
| 286 | + | ||
| 287 | +```json | ||
| 288 | +{ | ||
| 289 | + "ConditionRule": { | ||
| 290 | + "oneOf": [ | ||
| 291 | + { "$ref": "#/definitions/SimpleCondition" }, | ||
| 292 | + { "$ref": "#/definitions/CompositeCondition" } | ||
| 293 | + ] | ||
| 294 | + }, | ||
| 295 | + "SimpleCondition": { | ||
| 296 | + "type": "object", | ||
| 297 | + "required": ["field", "op", "value"], | ||
| 298 | + "properties": { | ||
| 299 | + "field": { | ||
| 300 | + "type": "string", | ||
| 301 | + "description": "依赖的字段 key" | ||
| 302 | + }, | ||
| 303 | + "op": { | ||
| 304 | + "$ref": "#/definitions/ConditionOperator", | ||
| 305 | + "description": "比较操作符" | ||
| 306 | + }, | ||
| 307 | + "value": { | ||
| 308 | + "description": "比较值", | ||
| 309 | + "oneOf": [ | ||
| 310 | + { "type": "string" }, | ||
| 311 | + { "type": "number" }, | ||
| 312 | + { "type": "boolean" }, | ||
| 313 | + { | ||
| 314 | + "type": "array", | ||
| 315 | + "items": { "type": "string" } | ||
| 316 | + } | ||
| 317 | + ] | ||
| 318 | + } | ||
| 319 | + } | ||
| 320 | + }, | ||
| 321 | + "CompositeCondition": { | ||
| 322 | + "type": "object", | ||
| 323 | + "required": ["op", "conditions"], | ||
| 324 | + "properties": { | ||
| 325 | + "op": { | ||
| 326 | + "enum": ["AND", "OR"], | ||
| 327 | + "description": "逻辑操作符" | ||
| 328 | + }, | ||
| 329 | + "conditions": { | ||
| 330 | + "type": "array", | ||
| 331 | + "items": { "$ref": "#/definitions/ConditionRule" }, | ||
| 332 | + "description": "子条件列表" | ||
| 333 | + } | ||
| 334 | + } | ||
| 335 | + }, | ||
| 336 | + "ConditionOperator": { | ||
| 337 | + "type": "string", | ||
| 338 | + "enum": ["eq", "ne", "gt", "lt", "gte", "lte", "in"], | ||
| 339 | + "description": "比较操作符" | ||
| 340 | + } | ||
| 341 | +} | ||
| 342 | +``` | ||
| 343 | + | ||
| 344 | +### 条件规则示例 | ||
| 345 | + | ||
| 346 | +```json | ||
| 347 | +{ | ||
| 348 | + "简单条件": { | ||
| 349 | + "field": "withdrawal_enabled", | ||
| 350 | + "op": "eq", | ||
| 351 | + "value": true | ||
| 352 | + }, | ||
| 353 | + "字符串相等": { | ||
| 354 | + "field": "withdrawal_mode", | ||
| 355 | + "op": "eq", | ||
| 356 | + "value": "指定提取金额" | ||
| 357 | + }, | ||
| 358 | + "数值范围": { | ||
| 359 | + "field": "age", | ||
| 360 | + "op": "gte", | ||
| 361 | + "value": 18 | ||
| 362 | + }, | ||
| 363 | + "包含于": { | ||
| 364 | + "field": "product_type", | ||
| 365 | + "op": "in", | ||
| 366 | + "value": ["A", "B", "C"] | ||
| 367 | + }, | ||
| 368 | + "复合条件": { | ||
| 369 | + "op": "AND", | ||
| 370 | + "conditions": [ | ||
| 371 | + { "field": "age", "op": "gte", "value": 18 }, | ||
| 372 | + { "field": "age", "op": "lt", "value": 65 } | ||
| 373 | + ] | ||
| 374 | + } | ||
| 375 | +} | ||
| 376 | +``` | ||
| 377 | + | ||
| 378 | +### 向后兼容的旧格式 | ||
| 379 | + | ||
| 380 | +```json | ||
| 381 | +{ | ||
| 382 | + "旧格式(自动转换)": { | ||
| 383 | + "field": "withdrawal_mode", | ||
| 384 | + "equals": "指定提取金额" | ||
| 385 | + }, | ||
| 386 | + "新格式": { | ||
| 387 | + "field": "withdrawal_mode", | ||
| 388 | + "op": "eq", | ||
| 389 | + "value": "指定提取金额" | ||
| 390 | + } | ||
| 391 | +} | ||
| 392 | +``` | ||
| 393 | + | ||
| 394 | +--- | ||
| 395 | + | ||
| 396 | +## SubmitMapping 规范 | ||
| 397 | + | ||
| 398 | +### SubmitMapping 根对象 | ||
| 399 | + | ||
| 400 | +```json | ||
| 401 | +{ | ||
| 402 | + "SubmitMapping": { | ||
| 403 | + "type": "object", | ||
| 404 | + "patternProperties": { | ||
| 405 | + "^[a-z_]+$": { | ||
| 406 | + "oneOf": [ | ||
| 407 | + { "type": "string" }, | ||
| 408 | + { "$ref": "#/definitions/FieldMapping" } | ||
| 409 | + ] | ||
| 410 | + } | ||
| 411 | + }, | ||
| 412 | + "description": "字段键名到 API 字段映射的字典" | ||
| 413 | + }, | ||
| 414 | + "FieldMapping": { | ||
| 415 | + "type": "object", | ||
| 416 | + "required": ["api_field"], | ||
| 417 | + "properties": { | ||
| 418 | + "api_field": { | ||
| 419 | + "type": "string", | ||
| 420 | + "description": "API 接口字段名" | ||
| 421 | + }, | ||
| 422 | + "transform": { | ||
| 423 | + "type": "string", | ||
| 424 | + "enum": ["fen_to_yuan", "yuan_to_fen", "none"], | ||
| 425 | + "description": "值转换类型", | ||
| 426 | + "default": "none" | ||
| 427 | + } | ||
| 428 | + } | ||
| 429 | + } | ||
| 430 | +} | ||
| 431 | +``` | ||
| 432 | + | ||
| 433 | +### SubmitMapping 示例 | ||
| 434 | + | ||
| 435 | +```json | ||
| 436 | +{ | ||
| 437 | + "customer_name": { | ||
| 438 | + "api_field": "customer_name" | ||
| 439 | + }, | ||
| 440 | + "gender": { | ||
| 441 | + "api_field": "customer_gender" | ||
| 442 | + }, | ||
| 443 | + "coverage": { | ||
| 444 | + "api_field": "annual_premium", | ||
| 445 | + "transform": "fen_to_yuan" | ||
| 446 | + }, | ||
| 447 | + "annual_withdrawal_amount": { | ||
| 448 | + "api_field": "annual_withdrawal_amount", | ||
| 449 | + "transform": "fen_to_yuan" | ||
| 450 | + }, | ||
| 451 | + "withdrawal_start_age_specified": { | ||
| 452 | + "api_field": "withdrawal_start_age" | ||
| 453 | + }, | ||
| 454 | + "withdrawal_start_age_fixed": { | ||
| 455 | + "api_field": "withdrawal_start_age" | ||
| 456 | + } | ||
| 457 | +} | ||
| 458 | +``` | ||
| 459 | + | ||
| 460 | +### 简写格式 | ||
| 461 | + | ||
| 462 | +```json | ||
| 463 | +{ | ||
| 464 | + "简写格式(无需转换)": "customer_name", | ||
| 465 | + "等价于": { | ||
| 466 | + "api_field": "customer_name" | ||
| 467 | + } | ||
| 468 | +} | ||
| 469 | +``` | ||
| 470 | + | ||
| 471 | +--- | ||
| 472 | + | ||
| 473 | +## WithdrawalPlan 规范 | ||
| 474 | + | ||
| 475 | +```json | ||
| 476 | +{ | ||
| 477 | + "WithdrawalPlan": { | ||
| 478 | + "type": "object", | ||
| 479 | + "required": ["enabled"], | ||
| 480 | + "properties": { | ||
| 481 | + "enabled": { | ||
| 482 | + "type": "boolean", | ||
| 483 | + "description": "是否启用提取计划" | ||
| 484 | + }, | ||
| 485 | + "currencies": { | ||
| 486 | + "type": "array", | ||
| 487 | + "items": { "$ref": "#/definitions/CurrencyCode" }, | ||
| 488 | + "description": "支持的币种列表" | ||
| 489 | + }, | ||
| 490 | + "default_currency": { | ||
| 491 | + "$ref": "#/definitions/CurrencyCode", | ||
| 492 | + "description": "默认币种" | ||
| 493 | + }, | ||
| 494 | + "withdrawal_modes": { | ||
| 495 | + "type": "array", | ||
| 496 | + "items": { "type": "string" }, | ||
| 497 | + "description": "提取模式选项", | ||
| 498 | + "examples": [["指定提取金额", "最高固定提取金额"]] | ||
| 499 | + }, | ||
| 500 | + "withdrawal_periods": { | ||
| 501 | + "type": "array", | ||
| 502 | + "items": { "type": "string" }, | ||
| 503 | + "description": "提取年期选项", | ||
| 504 | + "examples": [["1年", "2年", "5年", "10年", "终身"]] | ||
| 505 | + } | ||
| 506 | + } | ||
| 507 | + } | ||
| 508 | +} | ||
| 509 | +``` | ||
| 510 | + | ||
| 511 | +--- | ||
| 512 | + | ||
| 513 | +## 完整配置示例 | ||
| 514 | + | ||
| 515 | +### 储蓄型产品配置 | ||
| 516 | + | ||
| 517 | +```json | ||
| 518 | +{ | ||
| 519 | + "form_sn": "savings-gs", | ||
| 520 | + "name": "宏挚传承保障计划", | ||
| 521 | + "component": "SavingsTemplate", | ||
| 522 | + "category": "savings", | ||
| 523 | + "config": { | ||
| 524 | + "currency": "USD", | ||
| 525 | + "payment_periods": ["整付", "3 年", "5 年", "10 年", "15 年"], | ||
| 526 | + "age_range": { "min": 0, "max": 100 }, | ||
| 527 | + "insurance_period": "终身", | ||
| 528 | + "withdrawal_plan": { | ||
| 529 | + "enabled": true, | ||
| 530 | + "currencies": ["HKD", "USD", "CNY"], | ||
| 531 | + "default_currency": "USD", | ||
| 532 | + "withdrawal_modes": ["指定提取金额", "最高固定提取金额"], | ||
| 533 | + "withdrawal_periods": ["1年", "2年", "3年", "5年", "10年", "15年", "20年", "终身"] | ||
| 534 | + }, | ||
| 535 | + "form_schema": { | ||
| 536 | + "base_fields": [ | ||
| 537 | + { | ||
| 538 | + "id": "customer_name", | ||
| 539 | + "key": "customer_name", | ||
| 540 | + "type": "name", | ||
| 541 | + "label": "申请人", | ||
| 542 | + "placeholder": "请输入申请人", | ||
| 543 | + "required": true | ||
| 544 | + }, | ||
| 545 | + { | ||
| 546 | + "id": "gender", | ||
| 547 | + "key": "gender", | ||
| 548 | + "type": "radio", | ||
| 549 | + "label": "性别", | ||
| 550 | + "options": ["男", "女"], | ||
| 551 | + "required": true | ||
| 552 | + }, | ||
| 553 | + { | ||
| 554 | + "id": "coverage", | ||
| 555 | + "key": "coverage", | ||
| 556 | + "type": "amount", | ||
| 557 | + "label": "年缴保费", | ||
| 558 | + "placeholder": "请输入年缴保费", | ||
| 559 | + "input_label": "请输入年缴保费金额", | ||
| 560 | + "required": true, | ||
| 561 | + "currency_from": "currency" | ||
| 562 | + }, | ||
| 563 | + { | ||
| 564 | + "id": "payment_period", | ||
| 565 | + "key": "payment_period", | ||
| 566 | + "type": "payment_period", | ||
| 567 | + "label": "缴费年期", | ||
| 568 | + "required": true, | ||
| 569 | + "options_from": "payment_periods" | ||
| 570 | + } | ||
| 571 | + ], | ||
| 572 | + "withdrawal_fields": [ | ||
| 573 | + { | ||
| 574 | + "id": "withdrawal_enabled", | ||
| 575 | + "key": "withdrawal_enabled", | ||
| 576 | + "type": "radio", | ||
| 577 | + "label": "是否希望生成一份允许减少名义金额的提取说明?", | ||
| 578 | + "options": ["是", "否"], | ||
| 579 | + "required": true, | ||
| 580 | + "default": "否" | ||
| 581 | + }, | ||
| 582 | + { | ||
| 583 | + "id": "withdrawal_mode", | ||
| 584 | + "key": "withdrawal_mode", | ||
| 585 | + "type": "radio", | ||
| 586 | + "label": "提取选项", | ||
| 587 | + "options": ["指定提取金额", "最高固定提取金额"], | ||
| 588 | + "required": true, | ||
| 589 | + "default": "指定提取金额", | ||
| 590 | + "section_title": "款项提取(允许减少名义金额)", | ||
| 591 | + "clear_when_hidden": true, | ||
| 592 | + "show_when": { | ||
| 593 | + "field": "withdrawal_enabled", | ||
| 594 | + "op": "eq", | ||
| 595 | + "value": "是" | ||
| 596 | + } | ||
| 597 | + }, | ||
| 598 | + { | ||
| 599 | + "id": "annual_withdrawal_amount", | ||
| 600 | + "key": "annual_withdrawal_amount", | ||
| 601 | + "type": "amount", | ||
| 602 | + "label": "每年提取金额", | ||
| 603 | + "placeholder": "请输入每年提取金额", | ||
| 604 | + "input_label": "请输入每年提取金额", | ||
| 605 | + "required": true, | ||
| 606 | + "currency_from": "withdrawal_plan.default_currency", | ||
| 607 | + "clear_when_hidden": true, | ||
| 608 | + "show_when": { | ||
| 609 | + "field": "withdrawal_mode", | ||
| 610 | + "op": "eq", | ||
| 611 | + "value": "指定提取金额" | ||
| 612 | + } | ||
| 613 | + } | ||
| 614 | + ] | ||
| 615 | + }, | ||
| 616 | + "submit_mapping": { | ||
| 617 | + "customer_name": { "api_field": "customer_name" }, | ||
| 618 | + "gender": { "api_field": "customer_gender" }, | ||
| 619 | + "coverage": { "api_field": "annual_premium", "transform": "fen_to_yuan" }, | ||
| 620 | + "payment_period": { "api_field": "payment_years" }, | ||
| 621 | + "withdrawal_enabled": { "api_field": "allow_reduce_amount" }, | ||
| 622 | + "withdrawal_mode": { "api_field": "withdrawal_option" }, | ||
| 623 | + "annual_withdrawal_amount": { "api_field": "annual_withdrawal_amount", "transform": "fen_to_yuan" } | ||
| 624 | + } | ||
| 625 | + } | ||
| 626 | +} | ||
| 627 | +``` | ||
| 628 | + | ||
| 629 | +--- | ||
| 630 | + | ||
| 631 | +## 验证工具 | ||
| 632 | + | ||
| 633 | +### Python JSON Schema 验证 | ||
| 634 | + | ||
| 635 | +```python | ||
| 636 | +import json | ||
| 637 | +from jsonschema import validate, ValidationError | ||
| 638 | + | ||
| 639 | +# 定义 Schema (使用上面定义的 JSON Schema) | ||
| 640 | +PLAN_CONFIG_SCHEMA = { | ||
| 641 | + "$schema": "http://json-schema.org/draft-07/schema#", | ||
| 642 | + "title": "计划书模板配置", | ||
| 643 | + "type": "object", | ||
| 644 | + "required": ["form_sn", "name", "component", "config"], | ||
| 645 | + "properties": { | ||
| 646 | + "form_sn": { | ||
| 647 | + "type": "string", | ||
| 648 | + "pattern": "^(life-insurance|critical-illness|savings)-[a-z0-9-]+$" | ||
| 649 | + }, | ||
| 650 | + "name": { "type": "string" }, | ||
| 651 | + "component": { | ||
| 652 | + "type": "string", | ||
| 653 | + "enum": ["LifeInsuranceTemplate", "CriticalIllnessTemplate", "SavingsTemplate"] | ||
| 654 | + }, | ||
| 655 | + "config": { "type": "object" } | ||
| 656 | + } | ||
| 657 | +} | ||
| 658 | + | ||
| 659 | +def validate_plan_config(config): | ||
| 660 | + """验证计划书配置""" | ||
| 661 | + try: | ||
| 662 | + validate(instance=config, schema=PLAN_CONFIG_SCHEMA) | ||
| 663 | + return True, "配置有效" | ||
| 664 | + except ValidationError as e: | ||
| 665 | + return False, f"验证失败: {e.message}" | ||
| 666 | + | ||
| 667 | +# 使用示例 | ||
| 668 | +with open('plan-config.json') as f: | ||
| 669 | + config = json.load(f) | ||
| 670 | + | ||
| 671 | +is_valid, message = validate_plan_config(config) | ||
| 672 | +print(f"验证结果: {is_valid}, {message}") | ||
| 673 | +``` | ||
| 674 | + | ||
| 675 | +### JavaScript JSON Schema 验证 | ||
| 676 | + | ||
| 677 | +```javascript | ||
| 678 | +import Ajv from 'ajv' | ||
| 679 | + | ||
| 680 | +const schema = { | ||
| 681 | + type: 'object', | ||
| 682 | + required: ['form_sn', 'name', 'component', 'config'], | ||
| 683 | + properties: { | ||
| 684 | + form_sn: { | ||
| 685 | + type: 'string', | ||
| 686 | + pattern: '^(life-insurance|critical-illness|savings)-[a-z0-9-]+$' | ||
| 687 | + }, | ||
| 688 | + name: { type: 'string' }, | ||
| 689 | + component: { | ||
| 690 | + type: 'string', | ||
| 691 | + enum: ['LifeInsuranceTemplate', 'CriticalIllnessTemplate', 'SavingsTemplate'] | ||
| 692 | + }, | ||
| 693 | + config: { type: 'object' } | ||
| 694 | + } | ||
| 695 | +} | ||
| 696 | + | ||
| 697 | +const ajv = new Ajv() | ||
| 698 | +const validate = ajv.compile(schema) | ||
| 699 | + | ||
| 700 | +function validatePlanConfig(config) { | ||
| 701 | + const valid = validate(config) | ||
| 702 | + if (!valid) { | ||
| 703 | + console.error('验证失败:', validate.errors) | ||
| 704 | + return false | ||
| 705 | + } | ||
| 706 | + return true | ||
| 707 | +} | ||
| 708 | + | ||
| 709 | +// 使用示例 | ||
| 710 | +const config = { /* ... */ } | ||
| 711 | +console.log(validatePlanConfig(config)) | ||
| 712 | +``` | ||
| 713 | + | ||
| 714 | +--- | ||
| 715 | + | ||
| 716 | +## TypeScript 类型定义 | ||
| 717 | + | ||
| 718 | +```typescript | ||
| 719 | +/** | ||
| 720 | + * 计划书配置类型定义 | ||
| 721 | + * 用于前端类型检查和后端 API 接口定义 | ||
| 722 | + */ | ||
| 723 | + | ||
| 724 | +/** 币种代码 */ | ||
| 725 | +type CurrencyCode = 'CNY' | 'USD' | 'HKD' | 'EUR' | ||
| 726 | + | ||
| 727 | +/** 字段类型 */ | ||
| 728 | +type FieldType = | ||
| 729 | + | 'name' | ||
| 730 | + | 'text' | ||
| 731 | + | 'amount' | ||
| 732 | + | 'percentage' | ||
| 733 | + | 'date' | ||
| 734 | + | 'age' | ||
| 735 | + | 'radio' | ||
| 736 | + | 'select' | ||
| 737 | + | 'payment_period' | ||
| 738 | + | ||
| 739 | +/** 条件操作符 */ | ||
| 740 | +type ConditionOperator = 'eq' | 'ne' | 'gt' | 'lt' | 'gte' | 'lte' | 'in' | ||
| 741 | + | ||
| 742 | +/** 值转换类型 */ | ||
| 743 | +type TransformType = 'fen_to_yuan' | 'yuan_to_fen' | 'none' | ||
| 744 | + | ||
| 745 | +/** 年龄范围 */ | ||
| 746 | +interface AgeRange { | ||
| 747 | + min: number | ||
| 748 | + max: number | ||
| 749 | +} | ||
| 750 | + | ||
| 751 | +/** 提取计划配置 */ | ||
| 752 | +interface WithdrawalPlan { | ||
| 753 | + enabled: boolean | ||
| 754 | + currencies?: CurrencyCode[] | ||
| 755 | + default_currency?: CurrencyCode | ||
| 756 | + withdrawal_modes?: string[] | ||
| 757 | + withdrawal_periods?: string[] | ||
| 758 | +} | ||
| 759 | + | ||
| 760 | +/** 条件规则 */ | ||
| 761 | +type ConditionRule = SimpleCondition | CompositeCondition | ||
| 762 | + | ||
| 763 | +interface SimpleCondition { | ||
| 764 | + field: string | ||
| 765 | + op: ConditionOperator | ||
| 766 | + value: string | number | boolean | string[] | ||
| 767 | +} | ||
| 768 | + | ||
| 769 | +interface CompositeCondition { | ||
| 770 | + op: 'AND' | 'OR' | ||
| 771 | + conditions: ConditionRule[] | ||
| 772 | +} | ||
| 773 | + | ||
| 774 | +/** 清空隐藏配置 */ | ||
| 775 | +type ClearWhenHidden = | ||
| 776 | + | boolean | ||
| 777 | + | null | ||
| 778 | + | { | ||
| 779 | + clear_self?: boolean | ||
| 780 | + clear_dependents?: string[] | ||
| 781 | + } | ||
| 782 | + | ||
| 783 | +/** 字段定义 */ | ||
| 784 | +interface FieldDefinition { | ||
| 785 | + id: string | ||
| 786 | + key: string | ||
| 787 | + type: FieldType | ||
| 788 | + label: string | ||
| 789 | + placeholder?: string | ||
| 790 | + input_label?: string | ||
| 791 | + required?: boolean | ||
| 792 | + default?: string | boolean | number | ||
| 793 | + options?: string[] | ||
| 794 | + options_from?: string | ||
| 795 | + currency_from?: string | ||
| 796 | + section_title?: string | ||
| 797 | + show_when?: ConditionRule | ||
| 798 | + clear_when_hidden?: ClearWhenHidden | ||
| 799 | +} | ||
| 800 | + | ||
| 801 | +/** 表单 Schema */ | ||
| 802 | +interface FormSchema { | ||
| 803 | + base_fields: FieldDefinition[] | ||
| 804 | + withdrawal_fields?: FieldDefinition[] | ||
| 805 | +} | ||
| 806 | + | ||
| 807 | +/** 字段映射 */ | ||
| 808 | +interface FieldMapping { | ||
| 809 | + api_field: string | ||
| 810 | + transform?: TransformType | ||
| 811 | +} | ||
| 812 | + | ||
| 813 | +/** 提交映射(字典) */ | ||
| 814 | +type SubmitMapping = Record<string, string | FieldMapping> | ||
| 815 | + | ||
| 816 | +/** 模板配置数据 */ | ||
| 817 | +interface TemplateConfigData { | ||
| 818 | + currency: CurrencyCode | ||
| 819 | + payment_periods: string[] | ||
| 820 | + age_range: AgeRange | ||
| 821 | + insurance_period?: string | ||
| 822 | + withdrawal_plan?: WithdrawalPlan | ||
| 823 | + form_schema: FormSchema | ||
| 824 | + submit_mapping: SubmitMapping | ||
| 825 | +} | ||
| 826 | + | ||
| 827 | +/** 模板配置(完整) */ | ||
| 828 | +interface TemplateConfig { | ||
| 829 | + form_sn: string | ||
| 830 | + name: string | ||
| 831 | + component: 'LifeInsuranceTemplate' | 'CriticalIllnessTemplate' | 'SavingsTemplate' | ||
| 832 | + category?: 'life-insurance' | 'critical-illness' | 'savings' | ||
| 833 | + config: TemplateConfigData | ||
| 834 | +} | ||
| 835 | +``` | ||
| 836 | + | ||
| 837 | +--- | ||
| 838 | + | ||
| 839 | +## 相关文档 | ||
| 840 | + | ||
| 841 | +- [后端迁移指南](./plan-backend-migration-guide.md) | ||
| 842 | +- [录入架构设计](./plan-entry-architecture.md) | ||
| 843 | +- [Schema 使用指南](./plan-form-schema-usage.md) |
-
Please register or login to post a comment