Переглянути джерело

feat(design): 优化穴位定位算法设计

- 增加男女差别:男性肩宽权重0.6,女性身长权重0.6
- 新增输入数据验证章节(第五章)
- 细化置信度模型(第六章):按穴位独立计算
- 移除传感器反馈相关逻辑(治疗过程中无传感器)
- 新增估算公式适用性检查
- 新增验证API端点:POST /api/users/:id/acupoint-table/validate
liaoxg 5 місяців тому
батько
коміт
2b7994c940
1 змінених файлів з 643 додано та 38 видалено
  1. 643 38
      openspec/changes/acupoint-location/design.md

+ 643 - 38
openspec/changes/acupoint-location/design.md

@@ -196,43 +196,105 @@ const ACUPOINT_OFFSETS: AcupointOffset[] = [
 ```typescript
 /**
  * 当用户未提供指寸数据时,根据肩宽和身长估算
+ * 考虑男女体型差异:男性肩宽影响更大,女性身长影响更大
  */
 function estimateFingerWidth(
   shoulderWidth: number,
   bodyLength: number,
   gender: 'male' | 'female'
 ): FingerWidth {
-  // 标准体型参考(中国成年人平均数据)
+  // 标准体型参考(中国成年人平均数据,基于 GB/T 10000-1988
   const STANDARD = {
     male: {
-      shoulderWidth: 42,
-      bodyLength: 85,
-      cun1: 2.0,    // 一寸约2cm
-      cun1_5: 3.0,  // 1.5寸约3cm
-      cun3: 6.0     // 三寸约6cm
+      shoulderWidth: 42,      // 男性平均肩宽(厘米)
+      bodyLength: 85,         // 男性平均坐高(厘米)
+      cun1: 2.2,              // 男性一寸约2.2cm
+      cun1_5: 3.3,            // 男性1.5寸约3.3cm
+      cun3: 6.5,              // 男性三寸约6.5cm
+      // 男性特征:肩宽差异更显著
+      shoulderWeight: 0.6,    // 肩宽权重
+      lengthWeight: 0.4       // 身长权重
     },
     female: {
-      shoulderWidth: 36,
-      bodyLength: 78,
-      cun1: 1.8,
-      cun1_5: 2.7,
-      cun3: 5.4
+      shoulderWidth: 36,      // 女性平均肩宽(厘米)
+      bodyLength: 78,         // 女性平均坐高(厘米)
+      cun1: 1.9,              // 女性一寸约1.9cm
+      cun1_5: 2.8,            // 女性1.5寸约2.8cm
+      cun3: 5.6,              // 女性三寸约5.6cm
+      // 女性特征:身长差异更显著
+      shoulderWeight: 0.4,    // 肩宽权重
+      lengthWeight: 0.6       // 身长权重
     }
   };
 
   const ref = STANDARD[gender];
 
-  // 综合缩放因子(肩宽和身长的平均)
+  // 分性别加权缩放因子
+  // 男性:肩宽权重更高(0.6),因为男性肩宽差异对手指粗细影响更明显
+  // 女性:身长权重更高(0.6),因为女性体型比例更依赖身高
   const widthScale = shoulderWidth / ref.shoulderWidth;
   const lengthScale = bodyLength / ref.bodyLength;
-  const avgScale = (widthScale + lengthScale) / 2;
-
+  
+  // 加权平均(考虑性别差异)
+  const weightedScale = 
+    widthScale * ref.shoulderWeight + 
+    lengthScale * ref.lengthWeight;
+
+  // 指寸比例关系:三寸≈一寸×3,1.5寸≈一寸×1.5
+  // 确保估算结果符合中医指寸定义
+  const estimatedCun1 = ref.cun1 * weightedScale;
+  
   return {
-    cun1: ref.cun1 * avgScale,
-    cun1_5: ref.cun1_5 * avgScale,
-    cun3: ref.cun3 * avgScale
+    cun1: estimatedCun1,
+    cun1_5: estimatedCun1 * 1.5,   // 严格按比例
+    cun3: estimatedCun1 * 3        // 严格按比例
   };
 }
+
+/**
+ * 估算公式的适用性检查
+ * 返回估算结果的可靠性评估
+ */
+function assessEstimationReliability(
+  shoulderWidth: number,
+  bodyLength: number,
+  gender: 'male' | 'female'
+): { reliable: boolean; warning?: string } {
+  
+  const RANGES = {
+    male: { shoulder: [36, 52], bodyLength: [75, 95] },
+    female: { shoulder: [32, 46], bodyLength: [70, 88] }
+  };
+  
+  const range = RANGES[gender];
+  
+  // 检查是否在正常范围内
+  const shoulderInRange = shoulderWidth >= range.shoulder[0] && shoulderWidth <= range.shoulder[1];
+  const bodyLengthInRange = bodyLength >= range.bodyLength[0] && bodyLength <= range.bodyLength[1];
+  
+  if (!shoulderInRange && !bodyLengthInRange) {
+    return {
+      reliable: false,
+      warning: '肩宽和身长均超出正常范围,估算结果可能不准确,建议提供实测指寸数据'
+    };
+  }
+  
+  if (!shoulderInRange) {
+    return {
+      reliable: true,
+      warning: '肩宽超出正常范围,建议提供实测指寸数据以提高精度'
+    };
+  }
+  
+  if (!bodyLengthInRange) {
+    return {
+      reliable: true,
+      warning: '身长超出正常范围,建议提供实测指寸数据以提高精度'
+    };
+  }
+  
+  return { reliable: true };
+}
 ```
 
 ### 4.2 穴位对照表生成算法
@@ -309,17 +371,288 @@ function calculateActualPositions(
 
 ---
 
-## 五、置信度计算
+## 五、输入数据验证
+
+### 5.1 验证规则
+
+```typescript
+// 验证结果
+interface ValidationResult {
+  isValid: boolean;
+  errors: ValidationError[];
+  warnings: ValidationWarning[];
+  suggestions: string[];
+}
+
+interface ValidationError {
+  field: string;
+  message: string;
+  actualValue: number;
+  expectedRange: { min: number; max: number };
+}
+
+interface ValidationWarning {
+  field: string;
+  message: string;
+  suggestion: string;
+}
+```
+
+### 5.2 身体数据验证
+
+```typescript
+/**
+ * 验证用户输入的身体数据
+ */
+function validateBodyData(bodyData: UserBodyData, gender: 'male' | 'female'): ValidationResult {
+  const errors: ValidationError[] = [];
+  const warnings: ValidationWarning[] = [];
+  const suggestions: string[] = [];
+
+  // 身体数据范围(基于 GB/T 10000-1988)
+  const RANGES = {
+    male: {
+      shoulderWidth: { min: 36, max: 52, typical: 42 },  // 单位:厘米
+      bodyLength: { min: 75, max: 95, typical: 85 },      // 坐高
+      cun1: { min: 1.5, max: 2.8, typical: 2.0 },
+      cun1_5: { min: 2.2, max: 4.0, typical: 3.0 },
+      cun3: { min: 4.5, max: 8.0, typical: 6.0 }
+    },
+    female: {
+      shoulderWidth: { min: 32, max: 46, typical: 36 },
+      bodyLength: { min: 70, max: 88, typical: 78 },
+      cun1: { min: 1.3, max: 2.4, typical: 1.8 },
+      cun1_5: { min: 2.0, max: 3.5, typical: 2.7 },
+      cun3: { min: 4.0, max: 7.0, typical: 5.4 }
+    }
+  };
+
+  const range = RANGES[gender];
+
+  // 验证肩宽
+  if (bodyData.shoulderWidth < range.shoulderWidth.min || bodyData.shoulderWidth > range.shoulderWidth.max) {
+    errors.push({
+      field: 'shoulderWidth',
+      message: '肩宽数值超出正常范围',
+      actualValue: bodyData.shoulderWidth,
+      expectedRange: range.shoulderWidth
+    });
+  }
+
+  // 验证身长(坐高)
+  if (bodyData.bodyLength < range.bodyLength.min || bodyData.bodyLength > range.bodyLength.max) {
+    errors.push({
+      field: 'bodyLength',
+      message: '身长数值超出正常范围',
+      actualValue: bodyData.bodyLength,
+      expectedRange: range.bodyLength
+    });
+  }
+
+  // 验证指寸数据(如果提供)
+  if (bodyData.fingerWidth) {
+    const fw = bodyData.fingerWidth;
+
+    // 验证一寸
+    if (fw.cun1 < range.cun1.min || fw.cun1 > range.cun1.max) {
+      warnings.push({
+        field: 'fingerWidth.cun1',
+        message: '一寸数值异常',
+        suggestion: `正常范围:${range.cun1.min}-${range.cun1.max}cm,您输入的是 ${fw.cun1}cm`
+      });
+    }
+
+    // 验证1.5寸
+    if (fw.cun1_5 < range.cun1_5.min || fw.cun1_5 > range.cun1_5.max) {
+      warnings.push({
+        field: 'fingerWidth.cun1_5',
+        message: '1.5寸数值异常',
+        suggestion: `正常范围:${range.cun1_5.min}-${range.cun1_5.max}cm,您输入的是 ${fw.cun1_5}cm`
+      });
+    }
+
+    // 验证三寸
+    if (fw.cun3 < range.cun3.min || fw.cun3 > range.cun3.max) {
+      warnings.push({
+        field: 'fingerWidth.cun3',
+        message: '三寸数值异常',
+        suggestion: `正常范围:${range.cun3.min}-${range.cun3.max}cm,您输入的是 ${fw.cun3}cm`
+      });
+    }
+
+    // 内部一致性检查:三寸应该约等于一寸的3倍
+    const cun3Expected = fw.cun1 * 3;
+    const cun3Diff = Math.abs(fw.cun3 - cun3Expected);
+    if (cun3Diff > fw.cun1 * 0.3) { // 允许30%偏差
+      warnings.push({
+        field: 'fingerWidth',
+        message: '指寸数据不一致',
+        suggestion: `三寸应该约为一寸的3倍(${cun3Expected.toFixed(1)}cm),您输入的三寸是 ${fw.cun3}cm`
+      });
+    }
+
+    // 1.5寸应该约等于一寸的1.5倍
+    const cun1_5Expected = fw.cun1 * 1.5;
+    const cun1_5Diff = Math.abs(fw.cun1_5 - cun1_5Expected);
+    if (cun1_5Diff > fw.cun1 * 0.25) { // 允许25%偏差
+      warnings.push({
+        field: 'fingerWidth',
+        message: '指寸数据不一致',
+        suggestion: `1.5寸应该约为一寸的1.5倍(${cun1_5Expected.toFixed(1)}cm),您输入的是 ${fw.cun1_5}cm`
+      });
+    }
+  }
+
+  // 生成建议
+  if (errors.length > 0) {
+    suggestions.push('请检查输入数据是否正确,参考测量指南重新测量');
+  }
+  if (warnings.length > 0 && bodyData.fingerWidth) {
+    suggestions.push('建议重新测量指寸数据,确保测量方法正确');
+  }
+
+  return {
+    isValid: errors.length === 0,
+    errors,
+    warnings,
+    suggestions
+  };
+}
+```
+
+### 5.3 测量异常处理
+
+```typescript
+/**
+ * 处理测量异常,提供修正建议
+ */
+function handleMeasurementAnomaly(
+  validationResult: ValidationResult,
+  bodyData: UserBodyData,
+  gender: 'male' | 'female'
+): { action: 'reject' | 'accept_with_warning' | 'auto_correct'; correctedData?: UserBodyData } {
+  
+  // 有错误数据,拒绝处理
+  if (validationResult.errors.length > 0) {
+    return { action: 'reject' };
+  }
+
+  // 有警告但无错误,检查严重程度
+  if (validationResult.warnings.length > 0) {
+    // 如果只是轻微不一致,接受但警告
+    const hasSevereWarning = validationResult.warnings.some(
+      w => w.field === 'fingerWidth' && w.message.includes('不一致')
+    );
+
+    if (!hasSevereWarning) {
+      return { action: 'accept_with_warning' };
+    }
+  }
+
+  return { action: 'accept_with_warning' };
+}
+```
+
+### 5.4 实时输入验证(前端)
+
+```typescript
+/**
+ * 输入时实时验证(用于前端表单)
+ */
+function validateInputField(
+  field: 'shoulderWidth' | 'bodyLength' | 'cun1' | 'cun1_5' | 'cun3',
+  value: number,
+  gender: 'male' | 'female'
+): { valid: boolean; message?: string } {
+  
+  const RANGES = {
+    male: {
+      shoulderWidth: { min: 36, max: 52 },
+      bodyLength: { min: 75, max: 95 },
+      cun1: { min: 1.5, max: 2.8 },
+      cun1_5: { min: 2.2, max: 4.0 },
+      cun3: { min: 4.5, max: 8.0 }
+    },
+    female: {
+      shoulderWidth: { min: 32, max: 46 },
+      bodyLength: { min: 70, max: 88 },
+      cun1: { min: 1.3, max: 2.4 },
+      cun1_5: { min: 2.0, max: 3.5 },
+      cun3: { min: 4.0, max: 7.0 }
+    }
+  };
+
+  const range = RANGES[gender][field];
+  
+  if (value < range.min || value > range.max) {
+    return {
+      valid: false,
+      message: `正常范围:${range.min}-${range.max}cm`
+    };
+  }
+
+  return { valid: true };
+}
+```
+
+### 5.5 验证错误码
+
+| 错误码 | 描述 | 处理建议 |
+|--------|------|----------|
+| `E001` | 肩宽超出范围 | 检查测量单位是否为厘米 |
+| `E002` | 身长超出范围 | 确认为坐高而非身高 |
+| `E003` | 指寸数据缺失 | 建议用户提供指寸以获得更高精度 |
+| `W001` | 一寸数值异常 | 检查测量方法,参考测量指南 |
+| `W002` | 指寸数据不一致 | 建议重新测量全部指寸数据 |
+| `W003` | 指寸与体型比例不符 | 检查是否测量错误 |
+
+---
+
+## 六、置信度计算
+
+### 6.1 置信度数据结构
+
+```typescript
+// 单个穴位的置信度
+interface AcupointConfidence {
+  acupointId: string;
+  confidence: number;           // 0-1
+  accuracyRange: number;        // 精度范围(厘米)
+  factors: ConfidenceFactor[];  // 影响因素
+}
+
+interface ConfidenceFactor {
+  name: string;
+  impact: number;    // 正值提升,负值降低
+  description: string;
+}
+
+// 整体置信度报告
+interface ConfidenceReport {
+  overall: number;                         // 综合置信度
+  byAcupoint: Map<string, AcupointConfidence>;
+  dataQuality: DataQualityAssessment;
+  recommendations: string[];
+}
+
+interface DataQualityAssessment {
+  fingerWidthScore: number;    // 指寸数据质量(0-100)
+  bodyDataScore: number;       // 体型数据质量(0-100)
+  consistencyScore: number;    // 数据一致性(0-100)
+  overallScore: number;        // 综合质量分
+}
+```
+
+### 6.2 基础置信度计算
 
 ```typescript
 /**
- * 计算穴位定位的置信度
+ * 计算整体置信度(原有简化版,保留向后兼容)
  */
 function calculateConfidence(bodyData: UserBodyData): number {
-  let confidence = 0.5;  // 基础置信度(仅有肩宽、身长)
+  let confidence = 0.5; // 基础置信度(仅有肩宽、身长)
 
   if (bodyData.fingerWidth) {
-    // 有指寸数据,提升置信度
     if (bodyData.fingerWidth.cun1) confidence += 0.15;
     if (bodyData.fingerWidth.cun1_5) confidence += 0.15;
     if (bodyData.fingerWidth.cun3) confidence += 0.15;
@@ -329,21 +662,255 @@ function calculateConfidence(bodyData: UserBodyData): number {
 }
 ```
 
-### 置信度与精度对照表
+### 6.3 详细置信度计算(按穴位)
 
-| 数据完整度 | 置信度 | 预估精度 | 说明 |
-|-----------|--------|---------|------|
-| 仅肩宽+身长 | 50% | ±5cm | 指寸为估算值 |
-| + 一寸数据 | 65% | ±4cm | 部分指寸实测 |
-| + 1.5寸数据 | 80% | ±3cm | 大部分指寸实测 |
-| + 三寸数据(完整) | 95% | ±2cm | 指寸全部实测 |
-| + 传感器大椎穴检测 | 100% | ±1.5cm | 最优精度 |
+```typescript
+/**
+ * 计算每个穴位的独立置信度
+ */
+function calculateDetailedConfidence(
+  bodyData: UserBodyData,
+  gender: 'male' | 'female',
+  acupoints: CalculatedAcupoint[]
+): ConfidenceReport {
+  
+  const byAcupoint = new Map<string, AcupointConfidence>();
+  const recommendations: string[] = [];
+
+  // 计算数据质量评分
+  const dataQuality = assessDataQuality(bodyData, gender);
+
+  // 为每个穴位计算置信度
+  for (const acupoint of acupoints) {
+    const confidence = calculateAcupointConfidence(
+      acupoint,
+      bodyData,
+      gender,
+      dataQuality
+    );
+    byAcupoint.set(acupoint.id, confidence);
+  }
+
+  // 计算综合置信度
+  const overall = calculateOverallConfidence(byAcupoint, dataQuality);
+
+  // 生成建议
+  if (dataQuality.fingerWidthScore < 70) {
+    recommendations.push('建议提供指寸数据以提高定位精度');
+  }
+  if (dataQuality.consistencyScore < 80) {
+    recommendations.push('指寸数据存在不一致,建议重新测量');
+  }
+
+  return {
+    overall,
+    byAcupoint,
+    dataQuality,
+    recommendations
+  };
+}
+
+/**
+ * 评估数据质量
+ */
+function assessDataQuality(
+  bodyData: UserBodyData,
+  gender: 'male' | 'female'
+): DataQualityAssessment {
+  
+  let fingerWidthScore = 0;
+  let consistencyScore = 100;
+  let bodyDataScore = 100;
+
+  // 指寸数据评分
+  if (bodyData.fingerWidth) {
+    const fw = bodyData.fingerWidth;
+    let score = 0;
+    
+    // 每个指寸数据贡献33分
+    if (fw.cun1 > 0) score += 33;
+    if (fw.cun1_5 > 0) score += 33;
+    if (fw.cun3 > 0) score += 34;
+    
+    fingerWidthScore = score;
+
+    // 一致性检查
+    const cun3Expected = fw.cun1 * 3;
+    const cun3Ratio = Math.abs(fw.cun3 - cun3Expected) / fw.cun1;
+    if (cun3Ratio > 0.3) {
+      consistencyScore -= 20;
+    }
+
+    const cun1_5Expected = fw.cun1 * 1.5;
+    const cun1_5Ratio = Math.abs(fw.cun1_5 - cun1_5Expected) / fw.cun1;
+    if (cun1_5Ratio > 0.25) {
+      consistencyScore -= 15;
+    }
+  }
+
+  // 体型数据评分(检查是否在合理范围)
+  const RANGES = gender === 'male'
+    ? { shoulder: [36, 52], bodyLength: [75, 95] }
+    : { shoulder: [32, 46], bodyLength: [70, 88] };
+
+  if (bodyData.shoulderWidth < RANGES.shoulder[0] || bodyData.shoulderWidth > RANGES.shoulder[1]) {
+    bodyDataScore -= 30;
+  }
+  if (bodyData.bodyLength < RANGES.bodyLength[0] || bodyData.bodyLength > RANGES.bodyLength[1]) {
+    bodyDataScore -= 30;
+  }
+
+  const overallScore = (fingerWidthScore + bodyDataScore + consistencyScore) / 3;
+
+  return {
+    fingerWidthScore,
+    bodyDataScore,
+    consistencyScore,
+    overallScore
+  };
+}
+
+/**
+ * 计算单个穴位的置信度
+ */
+function calculateAcupointConfidence(
+  acupoint: CalculatedAcupoint,
+  bodyData: UserBodyData,
+  gender: 'male' | 'female',
+  dataQuality: DataQualityAssessment
+): AcupointConfidence {
+  
+  const factors: ConfidenceFactor[] = [];
+  let confidence = 0.5;
+
+  // 因素1:指寸数据完整性
+  if (bodyData.fingerWidth) {
+    const fw = bodyData.fingerWidth;
+    if (acupoint.cunType === 'cun1' && fw.cun1 > 0) {
+      confidence += 0.15;
+      factors.push({ name: '一寸实测', impact: 0.15, description: '使用实测一寸数据' });
+    }
+    if (acupoint.cunType === 'cun1_5' && fw.cun1_5 > 0) {
+      confidence += 0.20;  // 1.5寸对旁开穴位更关键
+      factors.push({ name: '1.5寸实测', impact: 0.20, description: '使用实测1.5寸数据' });
+    }
+    if (acupoint.cunType === 'cun3' && fw.cun3 > 0) {
+      confidence += 0.15;
+      factors.push({ name: '三寸实测', impact: 0.15, description: '使用实测三寸数据' });
+    }
+  }
+
+  // 因素2:穴位类型影响
+  // 督脉穴位(正中)比双侧穴位更可靠
+  if (acupoint.side === 'center') {
+    confidence += 0.05;
+    factors.push({ name: '督脉穴位', impact: 0.05, description: '正中线穴位定位更可靠' });
+  } else {
+    // 双侧穴位受横向偏移影响
+    confidence -= 0.03;
+    factors.push({ name: '双侧穴位', impact: -0.03, description: '横向偏移增加不确定性' });
+  }
+
+  // 因素3:距离大椎穴的远近
+  // 距离越远,累积误差越大
+  const distanceY = Math.abs(acupoint.offsetFromDazhui.y);
+  if (distanceY > 30) {  // 超过30cm
+    confidence -= 0.05;
+    factors.push({ name: '远距离穴位', impact: -0.05, description: '距离基准点较远' });
+  }
+
+  // 因素4:数据一致性惩罚
+  if (dataQuality.consistencyScore < 80) {
+    const penalty = (80 - dataQuality.consistencyScore) / 100;
+    confidence -= penalty;
+    factors.push({ name: '数据一致性', impact: -penalty, description: '指寸数据存在不一致' });
+  }
+
+  // 确保置信度在0-1范围内
+  confidence = Math.max(0, Math.min(1, confidence));
+
+  // 计算精度范围
+  const accuracyRange = calculateAccuracyRange(confidence);
+
+  return {
+    acupointId: acupoint.id,
+    confidence,
+    accuracyRange,
+    factors
+  };
+}
+
+/**
+ * 根据置信度计算精度范围
+ */
+function calculateAccuracyRange(confidence: number): number {
+  // 置信度 50% → ±5cm
+  // 置信度 95% → ±2cm
+  // 置信度 100% → ±1.5cm
+  // 线性插值
+  if (confidence >= 0.95) return 2.0;
+  if (confidence >= 0.80) return 3.0;
+  if (confidence >= 0.65) return 4.0;
+  return 5.0;
+}
+
+/**
+ * 计算综合置信度
+ */
+function calculateOverallConfidence(
+  byAcupoint: Map<string, AcupointConfidence>,
+  dataQuality: DataQualityAssessment
+): number {
+  
+  if (byAcupoint.size === 0) return 0;
+
+  // 加权平均:考虑穴位重要性
+  // 督脉穴位权重更高(定位基准)
+  let totalWeight = 0;
+  let weightedSum = 0;
+
+  for (const [id, conf] of byAcupoint) {
+    const weight = id.includes('left') || id.includes('right') ? 1.0 : 1.5;
+    weightedSum += conf.confidence * weight;
+    totalWeight += weight;
+  }
+
+  const avgConfidence = weightedSum / totalWeight;
+
+  // 数据质量修正
+  const qualityFactor = dataQuality.overallScore / 100;
+  
+  return Math.min(avgConfidence * (0.7 + 0.3 * qualityFactor), 1.0);
+}
+```
+
+### 6.5 置信度与精度对照表(更新)
+
+| 数据完整度 | 综合置信度 | 预估精度 | 适用穴位 | 说明 |
+|-----------|-----------|---------|---------|------|
+| 仅肩宽+身长 | 50% | ±5cm | 全部 | 指寸为估算值 |
+| + 一寸数据 | 65% | ±4cm | 督脉穴位 | 部分指寸实测 |
+| + 一寸+1.5寸 | 80% | ±3cm | 全部 | 大部分指寸实测 |
+| + 完整指寸 | 95% | ±2cm | 全部 | 指寸全部实测 |
+
+### 6.6 不同穴位的典型置信度
+
+| 穴位 | 典型置信度 | 影响因素 | 精度范围 |
+|------|-----------|---------|---------|
+| 大椎穴 | 100% | 基准点 | ±2cm |
+| 身柱穴 | 95% | 督脉正中,距离近 | ±2cm |
+| 至阳穴 | 92% | 督脉正中,距离中等 | ±2.5cm |
+| 命门穴 | 88% | 督脉正中,距离远 | ±3cm |
+| 风门穴(双) | 85% | 双侧,需横向偏移 | ±3cm |
+| 肺俞穴(双) | 83% | 双侧,需横向偏移 | ±3.5cm |
+| 肾俞穴(双) | 78% | 双侧,距离远 | ±4cm |
+| 八髎穴 | 80% | 正中,距离最远 | ±4cm |
 
 ---
 
-## 六、指寸测量指南
+## 、指寸测量指南
 
-### 6.1 测量方法
+### 7.1 测量方法
 
 | 指寸 | 测量方法 | 图示 |
 |------|---------|------|
@@ -351,7 +918,7 @@ function calculateConfidence(bodyData: UserBodyData): number {
 | 1.5寸 | 食指+中指并拢宽度 | ✌️ 两指 |
 | 三寸 | 四指并拢宽度(除拇指) | 🖖 四指 |
 
-### 6.2 测量注意事项
+### 7.2 测量注意事项
 
 1. **一寸**:拇指弯曲,测量指关节最宽处
 2. **1.5寸**:食指和中指自然并拢,测量两指总宽度
@@ -359,22 +926,60 @@ function calculateConfidence(bodyData: UserBodyData): number {
 
 ---
 
-## 、API 设计
+## 、API 设计
 
-### 7.1 端点列表
+### 8.1 端点列表
 
 | 方法 | 路径 | 描述 |
 |------|------|------|
 | POST | `/api/users/:id/acupoint-table` | 生成用户穴位对照表 |
+| POST | `/api/users/:id/acupoint-table/validate` | 验证输入数据(生成前预检查) |
 | GET | `/api/users/:id/acupoint-table` | 获取用户穴位对照表 |
 | POST | `/api/treatment-sessions` | 创建治疗会话 |
 | POST | `/api/treatment-sessions/:id/start` | 开始治疗(传入大椎穴位置) |
 | GET | `/api/acupoints` | 获取所有穴位基础数据 |
 | GET | `/api/acupoints/:id` | 获取单个穴位详情 |
 
-### 7.2 请求/响应示例
+### 8.2 请求/响应示例
 
 ```typescript
+// POST /api/users/:id/acupoint-table/validate
+// Request
+{
+  "bodyData": {
+    "shoulderWidth": 42,
+    "bodyLength": 85,
+    "fingerWidth": {
+      "cun1": 2.1,
+      "cun1_5": 3.2,
+      "cun3": 6.3
+    }
+  },
+  "gender": "male"
+}
+
+// Response (验证通过)
+{
+  "isValid": true,
+  "errors": [],
+  "warnings": [],
+  "suggestions": []
+}
+
+// Response (有警告)
+{
+  "isValid": true,
+  "errors": [],
+  "warnings": [
+    {
+      "field": "fingerWidth",
+      "message": "指寸数据不一致",
+      "suggestion": "三寸应该约为一寸的3倍(6.3cm),您输入的三寸是 5.5cm"
+    }
+  ],
+  "suggestions": ["建议重新测量指寸数据,确保测量方法正确"]
+}
+
 // POST /api/users/:id/acupoint-table
 // Request
 {
@@ -452,7 +1057,7 @@ function calculateConfidence(bodyData: UserBodyData): number {
 
 ---
 
-## 、技术选型
+## 、技术选型
 
 | 层级 | 技术 | 理由 |
 |------|------|------|
@@ -464,7 +1069,7 @@ function calculateConfidence(bodyData: UserBodyData): number {
 
 ---
 
-## 、实施步骤
+## 、实施步骤
 
 ### Wave 1: 数据层
 1. 创建指寸和穴位数据类型
@@ -489,7 +1094,7 @@ function calculateConfidence(bodyData: UserBodyData): number {
 
 ---
 
-## 十、参考标准
+## 十、参考标准
 
 - **GB/T 12346-2006**《腧穴名称与定位》
 - **GB/T 10000-1988**《中国成年人人体尺寸》