hookehuyr

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

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

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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&#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">
21 + <mxGeometry y="40" width="560" height="50" as="geometry"/>
22 + </mxCell>
23 +
24 + <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">
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&#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">
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&#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">
41 + <mxGeometry y="40" width="170" height="50" as="geometry"/>
42 + </mxCell>
43 +
44 + <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">
45 + <mxGeometry y="100" width="170" height="50" as="geometry"/>
46 + </mxCell>
47 +
48 + <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">
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 表&#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">
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&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">
85 + <mxGeometry y="120" width="560" height="40" as="geometry"/>
86 + </mxCell>
87 +
88 + <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">
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:完全后端化&#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">
185 + <mxGeometry y="40" width="360" height="60" as="geometry"/>
186 + </mxCell>
187 +
188 + <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">
189 + <mxGeometry y="100" width="360" height="60" as="geometry"/>
190 + </mxCell>
191 +
192 + <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">
193 + <mxGeometry y="160" width="360" height="40" as="geometry"/>
194 + </mxCell>
195 +
196 + <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">
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>
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)
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)