design.md 34 KB

设计方案:穴位定位功能

一、用户使用流程

1.1 完整流程图

┌─────────────────────────────────────────────────────────────────────┐
│ 第一阶段:创建用户档案(首次使用)                                    │
├─────────────────────────────────────────────────────────────────────┤
│  1. 输入基本信息                                                    │
│     - 姓名、性别、年龄                                              │
│     - 肩宽(用户自行测量)                                          │
│     - 身长(用户自行测量)                                          │
│     - 指寸(选填):一寸、1.5寸、三寸                                │
│                                                                     │
│  2. 系统生成【穴位对照表】                                          │
│     - 基于用户体型数据计算各穴位相对位置                             │
│     - 存储到用户档案                                                │
└─────────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────────┐
│ 第二阶段:艾灸治疗(每次使用)                                       │
├─────────────────────────────────────────────────────────────────────┤
│  3. 用户就座                                                        │
│     - 坐在艾灸椅上                                                  │
│     - 传感器检测用户肩高                                            │
│                                                                     │
│  4. 设备定位大椎穴                                                  │
│     - 传感器获取大椎穴实际位置                                      │
│     - 作为所有穴位的基准点                                          │
│                                                                     │
│  5. 用户选择治疗方案                                                │
│     - 选择功效(驱寒、祛湿等)                                      │
│     - 或自定义穴位组合                                              │
│                                                                     │
│  6. 开始艾灸                                                        │
│     - 设备根据【穴位对照表】+ 大椎穴位置                             │
│     - 计算各穴位实际坐标                                            │
│     - 控制灸头移动施灸                                              │
└─────────────────────────────────────────────────────────────────────┘

1.2 传感器作用

传感器 功能 说明
肩高传感器 检测大椎穴位置 第七颈椎棘突下,作为穴位定位基准
无人检测 检测用户是否在座 安全保护,4分钟无人自动关机

注意:肩宽由用户自行输入,传感器不检测肩宽。


二、数据模型

2.1 用户指寸数据

// 用户指寸(三寸同身寸)
interface FingerWidth {
  cun1: number;    // 一寸 = 拇指指关节横纹宽度
  cun1_5: number;  // 1.5寸 = 食指+中指并拢宽度
  cun3: number;    // 三寸 = 四指并拢宽度
}

2.2 用户体型数据

// 用户体型数据
interface UserBodyData {
  shoulderWidth: number;     // 肩宽(厘米),用户输入
  bodyLength: number;        // 身长(厘米),从头顶到坐骨结节
  fingerWidth?: FingerWidth; // 指寸(选填)
}

// 用户档案
interface UserProfile {
  id: string;
  name: string;
  gender: 'male' | 'female';
  age: number;
  bodyData: UserBodyData;
  acupointTable: AcupointReferenceTable;  // 穴位对照表
  createdAt: Date;
  updatedAt: Date;
}

2.3 穴位对照表

// 穴位对照表(根据用户体型预计算)
interface AcupointReferenceTable {
  userId: string;
  bodyData: UserBodyData;
  fingerWidthUsed: FingerWidth;  // 实际使用的指寸值(记录或估算)
  acupoints: CalculatedAcupoint[];
  createdAt: Date;
}

// 计算后的单个穴位
interface CalculatedAcupoint {
  id: string;           // 如 'dazhui', 'fengmen_left'
  name: string;         // 穴位名称
  side: 'center' | 'left' | 'right';
  offsetFromDazhui: {
    x: number;          // 相对大椎穴的横向偏移(厘米)
    y: number;          // 相对大椎穴的纵向偏移(厘米)
  };
  offsetInCun: number;  // 传统指寸表示
  cunType: 'cun1' | 'cun1_5' | 'cun3';
}

// 治疗会话
interface TreatmentSession {
  sessionId: string;
  userId: string;
  dazhuiPosition: { x: number; y: number };  // 传感器检测的大椎穴位置
  selectedAcupoints: string[];               // 用户选择的穴位
  status: 'preparing' | 'running' | 'paused' | 'completed';
}

三、穴位基础数据

3.1 十二常用穴位

序号 穴位名称 所属经络 相对大椎穴位置 功效
1 大椎穴 督脉 基准点 (0, 0) 驱寒、祛湿、解表
2 身柱穴 督脉 大椎穴下3寸 止咳平喘、安神
3 至阳穴 督脉 大椎穴下7寸 宽胸理气
4 命门穴 督脉 大椎穴下14寸 补肾壮阳
5 腰阳关穴 督脉 大椎穴下17寸 强腰健肾
6 风门穴(双) 足太阳膀胱经 大椎穴下1.5寸,旁开1.5寸 祛风解表
7 秉风穴(双) 手太阳小肠经 大椎穴下3寸,旁开3寸 祛风散寒
8 肺俞穴(双) 足太阳膀胱经 大椎穴下3寸,旁开1.5寸 化痰止咳
9 膈俞穴(双) 足太阳膀胱经 大椎穴下7寸,旁开1.5寸 活血化瘀
10 脾俞穴(双) 足太阳膀胱经 大椎穴下11寸,旁开1.5寸 健脾养血
11 肾俞穴(双) 足太阳膀胱经 大椎穴下14寸,旁开1.5寸 补肾益气
12 八髎穴 足太阳膀胱经 大椎穴下16寸 调理下焦

3.2 穴位偏移数据结构

// 穴位偏移基准数据(以指寸为单位)
interface AcupointOffset {
  id: string;
  name: string;
  cun: number;              // 距离大椎穴的指寸数
  cunType: 'cun1' | 'cun1_5' | 'cun3';  // 使用的指寸类型
  side: 'center' | 'left' | 'right';
  lateralOffset?: number;   // 横向偏移(指寸)
}

const ACUPOINT_OFFSETS: AcupointOffset[] = [
  // 督脉穴位(正中)
  { id: 'dazhui', name: '大椎穴', cun: 0, cunType: 'cun1', side: 'center' },
  { id: 'shenzhu', name: '身柱穴', cun: 3, cunType: 'cun1', side: 'center' },
  { id: 'zhiyang', name: '至阳穴', cun: 7, cunType: 'cun1', side: 'center' },
  { id: 'mingmen', name: '命门穴', cun: 14, cunType: 'cun1', side: 'center' },
  { id: 'yaoyangguan', name: '腰阳关穴', cun: 17, cunType: 'cun1', side: 'center' },
  
  // 膀胱经穴位(双侧)
  { id: 'fengmen', name: '风门穴', cun: 1.5, cunType: 'cun1_5', side: 'left', lateralOffset: 1.5 },
  { id: 'fengmen', name: '风门穴', cun: 1.5, cunType: 'cun1_5', side: 'right', lateralOffset: 1.5 },
  { id: 'feishu', name: '肺俞穴', cun: 3, cunType: 'cun1', side: 'left', lateralOffset: 1.5 },
  { id: 'feishu', name: '肺俞穴', cun: 3, cunType: 'cun1', side: 'right', lateralOffset: 1.5 },
  { id: 'geshu', name: '膈俞穴', cun: 7, cunType: 'cun1', side: 'left', lateralOffset: 1.5 },
  { id: 'geshu', name: '膈俞穴', cun: 7, cunType: 'cun1', side: 'right', lateralOffset: 1.5 },
  { id: 'pishu', name: '脾俞穴', cun: 11, cunType: 'cun1', side: 'left', lateralOffset: 1.5 },
  { id: 'pishu', name: '脾俞穴', cun: 11, cunType: 'cun1', side: 'right', lateralOffset: 1.5 },
  { id: 'shenshu', name: '肾俞穴', cun: 14, cunType: 'cun1', side: 'left', lateralOffset: 1.5 },
  { id: 'shenshu', name: '肾俞穴', cun: 14, cunType: 'cun1', side: 'right', lateralOffset: 1.5 },
  
  // 小肠经穴位(双侧)
  { id: 'bingfeng', name: '秉风穴', cun: 3, cunType: 'cun1', side: 'left', lateralOffset: 3 },
  { id: 'bingfeng', name: '秉风穴', cun: 3, cunType: 'cun1', side: 'right', lateralOffset: 3 },
  
  // 八髎穴
  { id: 'baliao', name: '八髎穴', cun: 16, cunType: 'cun1', side: 'center' },
];

四、核心算法

4.1 指寸估算算法

/**
 * 当用户未提供指寸数据时,根据肩宽和身长估算
 * 考虑男女体型差异:男性肩宽影响更大,女性身长影响更大
 */
function estimateFingerWidth(
  shoulderWidth: number,
  bodyLength: number,
  gender: 'male' | 'female'
): FingerWidth {
  // 标准体型参考(中国成年人平均数据,基于 GB/T 10000-1988)
  const STANDARD = {
    male: {
      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.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 weightedScale = 
    widthScale * ref.shoulderWeight + 
    lengthScale * ref.lengthWeight;

  // 指寸比例关系:三寸≈一寸×3,1.5寸≈一寸×1.5
  // 确保估算结果符合中医指寸定义
  const estimatedCun1 = ref.cun1 * weightedScale;
  
  return {
    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 穴位对照表生成算法

/**
 * 生成用户专属的穴位对照表
 */
function generateAcupointReferenceTable(
  bodyData: UserBodyData,
  gender: 'male' | 'female'
): AcupointReferenceTable {
  const { shoulderWidth, bodyLength, fingerWidth } = bodyData;

  // 获取或估算指寸
  const cun: FingerWidth = fingerWidth 
    ? fingerWidth 
    : estimateFingerWidth(shoulderWidth, bodyLength, gender);

  // 计算各穴位位置
  const acupoints: CalculatedAcupoint[] = ACUPOINT_OFFSETS.map(ap => {
    // Y方向偏移:指寸数 × 对应指寸值
    const distanceY = ap.cun * cun[ap.cunType];

    // X方向偏移:横向指寸 × 一寸值
    const distanceX = ap.side === 'center' 
      ? 0 
      : (ap.side === 'left' ? -1 : 1) * (ap.lateralOffset || 0) * cun.cun1;

    return {
      id: ap.id + (ap.side !== 'center' ? '_' + ap.side : ''),
      name: ap.name,
      side: ap.side,
      offsetFromDazhui: {
        x: distanceX,
        y: -distanceY  // 负值表示向下
      },
      offsetInCun: ap.cun,
      cunType: ap.cunType
    };
  });

  return {
    bodyData,
    fingerWidthUsed: cun,
    acupoints,
    createdAt: new Date()
  };
}

4.3 实时穴位定位算法

/**
 * 治疗时根据传感器检测的大椎穴位置计算实际穴位坐标
 */
function calculateActualPositions(
  referenceTable: AcupointReferenceTable,
  dazhuiPosition: { x: number; y: number }
): Map<string, { x: number; y: number }> {
  const positions = new Map<string, { x: number; y: number }>();

  for (const acupoint of referenceTable.acupoints) {
    positions.set(acupoint.id, {
      x: dazhuiPosition.x + acupoint.offsetFromDazhui.x,
      y: dazhuiPosition.y + acupoint.offsetFromDazhui.y
    });
  }

  return positions;
}

五、输入数据验证

5.1 验证规则

// 验证结果
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 身体数据验证

/**
 * 验证用户输入的身体数据
 */
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 测量异常处理

/**
 * 处理测量异常,提供修正建议
 */
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 实时输入验证(前端)

/**
 * 输入时实时验证(用于前端表单)
 */
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 置信度数据结构

// 单个穴位的置信度
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 基础置信度计算

/**
 * 计算整体置信度(原有简化版,保留向后兼容)
 */
function calculateConfidence(bodyData: UserBodyData): number {
  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;
  }

  return Math.min(confidence, 1.0);
}

6.3 详细置信度计算(按穴位)

/**
 * 计算每个穴位的独立置信度
 */
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

七、指寸测量指南

7.1 测量方法

指寸 测量方法 图示
一寸 拇指指关节横纹宽度 🤜 拇指
1.5寸 食指+中指并拢宽度 ✌️ 两指
三寸 四指并拢宽度(除拇指) 🖖 四指

7.2 测量注意事项

  1. 一寸:拇指弯曲,测量指关节最宽处
  2. 1.5寸:食指和中指自然并拢,测量两指总宽度
  3. 三寸:四指自然并拢,测量总宽度

八、API 设计

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 获取单个穴位详情

8.2 请求/响应示例

// 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
{
  "bodyData": {
    "shoulderWidth": 42,
    "bodyLength": 85,
    "fingerWidth": {
      "cun1": 2.1,
      "cun1_5": 3.2,
      "cun3": 6.3
    }
  }
}

// Response
{
  "userId": "user_001",
  "acupointTable": {
    "bodyData": { ... },
    "fingerWidthUsed": {
      "cun1": 2.1,
      "cun1_5": 3.2,
      "cun3": 6.3
    },
    "acupoints": [
      {
        "id": "dazhui",
        "name": "大椎穴",
        "side": "center",
        "offsetFromDazhui": { "x": 0, "y": 0 },
        "offsetInCun": 0,
        "cunType": "cun1"
      },
      {
        "id": "shenzhu",
        "name": "身柱穴",
        "side": "center",
        "offsetFromDazhui": { "x": 0, "y": -6.3 },
        "offsetInCun": 3,
        "cunType": "cun1"
      },
      {
        "id": "fengmen_left",
        "name": "风门穴",
        "side": "left",
        "offsetFromDazhui": { "x": -3.15, "y": -4.8 },
        "offsetInCun": 1.5,
        "cunType": "cun1_5"
      }
      // ... 其他穴位
    ],
    "createdAt": "2026-03-31T12:00:00Z"
  },
  "confidence": 0.95
}

// POST /api/treatment-sessions/:id/start
// Request
{
  "dazhuiPosition": { "x": 0, "y": 150 },  // 传感器检测的大椎穴位置
  "selectedAcupoints": ["dazhui", "shenzhu", "zhiyang"]
}

// Response
{
  "sessionId": "session_001",
  "actualPositions": {
    "dazhui": { "x": 0, "y": 150 },
    "shenzhu": { "x": 0, "y": 143.7 },
    "zhiyang": { "x": 0, "y": 135.9 }
  },
  "status": "running"
}

九、技术选型

层级 技术 理由
前端可视化 Canvas + SVG 经络图绘制,支持缩放、点击交互
状态管理 Zustand 穴位选择状态,轻量高效
数据格式 JSON 穴位数据存储,易于维护
坐标计算 TypeScript 类型安全,精度控制
API RESTful 与现有架构一致

十、实施步骤

Wave 1: 数据层

  1. 创建指寸和穴位数据类型
  2. 实现指寸估算算法
  3. 实现穴位对照表生成算法
  4. 编写单元测试

Wave 2: API 层

  1. 创建用户穴位对照表 API
  2. 创建治疗会话 API
  3. 编写 API 文档

Wave 3: 前端实现

  1. 用户指寸录入界面
  2. 穴位对照表展示
  3. 经络图可视化组件

Wave 4: 集成测试

  1. 与硬件传感器集成
  2. 端到端测试
  3. 验收测试

十一、参考标准

  • GB/T 12346-2006《腧穴名称与定位》
  • GB/T 10000-1988《中国成年人人体尺寸》
  • 《针灸学》教材(穴位定位方法)