|
|
@@ -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**《中国成年人人体尺寸》
|