|
|
@@ -18,97 +18,112 @@ import org.springframework.transaction.annotation.Transactional;
|
|
|
|
|
|
import java.math.BigDecimal;
|
|
|
import java.math.RoundingMode;
|
|
|
-import java.util.ArrayList;
|
|
|
-import java.util.Collections;
|
|
|
-import java.util.List;
|
|
|
+import java.util.*;
|
|
|
|
|
|
/**
|
|
|
- * 用户穴位坐标 Service 实现类
|
|
|
- * 核心功能:根据子用户体型数据(指宽/肩宽/身长)与穴位定位参数,计算个性化穴位坐标
|
|
|
+ * 用户穴位坐标 Service 实现类(算法3.0)
|
|
|
*
|
|
|
- * <p>坐标计算规则(相对定位):</p>
|
|
|
- * <ul>
|
|
|
- * <li>1寸(mm) = 对应指宽(cm)× 10</li>
|
|
|
- * <li>coordinateY(mm)= 纵向偏移(寸)× 1寸换算值</li>
|
|
|
- * <li>coordinateX(mm)= 横向偏移(寸)× 1寸换算值,中心穴位固定为 0</li>
|
|
|
- * <li>双侧穴位(side=4)生成 left/right 两条记录,X 取反对称</li>
|
|
|
- * </ul>
|
|
|
+ * <p>核心算法流程:</p>
|
|
|
+ * <ol>
|
|
|
+ * <li>三路指寸融合:A路(实测加权) + B路(BMI换算) → 最终指寸(mm)</li>
|
|
|
+ * <li>背部一寸宽度:肩宽推算 + 指寸验证 双源融合</li>
|
|
|
+ * <li>Y轴:Panjabi椎骨归一化位置 × 脊柱长度,回退为 纵向偏移(寸) × 最终指寸</li>
|
|
|
+ * <li>X轴:横向偏移 × 背部一寸宽度 × 区域系数;八髎穴采用骶后孔解剖基准偏移</li>
|
|
|
+ * <li>Z轴:体表 = 0</li>
|
|
|
+ * <li>置信度:双路→高,单路→中,无→低</li>
|
|
|
+ * </ol>
|
|
|
*/
|
|
|
@Service
|
|
|
public class UserAcupointServiceImpl extends ServiceImpl<UserAcupointMapper, UserAcupoint> implements UserAcupointService {
|
|
|
|
|
|
- /** 指宽未设置时使用的默认 1寸换算值(mm),取中医标准均值 18 mm */
|
|
|
- private static final BigDecimal DEFAULT_ONE_CUN_MM = new BigDecimal("18.0");
|
|
|
+ // ==================== 常量 ====================
|
|
|
+
|
|
|
+ /** 标准人BMI: 65kg / 1.70m^2 ≈ 22.49 */
|
|
|
+ private static final BigDecimal STANDARD_BMI = new BigDecimal("22.49");
|
|
|
+ /** 标准人指寸 24.0mm */
|
|
|
+ private static final BigDecimal STANDARD_CUN_MM = new BigDecimal("24.0");
|
|
|
+ /** 标准参考人身高 160cm(八髎穴基准偏移参照) */
|
|
|
+ private static final BigDecimal STANDARD_HEIGHT_CM = new BigDecimal("160.0");
|
|
|
+ /** 肩宽 → 肩胛骨内缘间距比例系数 */
|
|
|
+ private static final BigDecimal SCAPULA_RATIO = new BigDecimal("0.420");
|
|
|
+ /** 肩胛骨内缘间距对应寸数 = 6寸 */
|
|
|
+ private static final BigDecimal SCAPULA_CUN_COUNT = new BigDecimal("6");
|
|
|
+ /** 骶段区域收窄系数 */
|
|
|
+ private static final BigDecimal SACRAL_REGION_COEFFICIENT = new BigDecimal("0.85");
|
|
|
+ /** 判定骶段的 normalizedY 阈值(≥0.750 视为骶段) */
|
|
|
+ private static final BigDecimal SACRAL_Y_THRESHOLD = new BigDecimal("0.750");
|
|
|
+
|
|
|
+ private static final BigDecimal TWO = new BigDecimal("2");
|
|
|
+ private static final BigDecimal BD_1_5 = new BigDecimal("1.5");
|
|
|
+ private static final BigDecimal BD_3 = new BigDecimal("3");
|
|
|
+ private static final BigDecimal BD_100 = new BigDecimal("100");
|
|
|
+ private static final BigDecimal BD_50 = new BigDecimal("50");
|
|
|
+ private static final BigDecimal BD_25 = new BigDecimal("25");
|
|
|
+ private static final BigDecimal DIFF_THRESHOLD = new BigDecimal("0.15");
|
|
|
+
|
|
|
+ /** 八髎穴骶后孔基准偏移(mm),基于160cm标准参考人CT测量 */
|
|
|
+ private static final Map<String, BigDecimal> BALIAO_OFFSETS;
|
|
|
+ static {
|
|
|
+ Map<String, BigDecimal> m = new HashMap<>();
|
|
|
+ m.put("shangliao", new BigDecimal("15"));
|
|
|
+ m.put("ciliao", new BigDecimal("17"));
|
|
|
+ m.put("zhongliao", new BigDecimal("16"));
|
|
|
+ m.put("xialiao", new BigDecimal("11"));
|
|
|
+ BALIAO_OFFSETS = Collections.unmodifiableMap(m);
|
|
|
+ }
|
|
|
|
|
|
@Autowired
|
|
|
private UserProfileMapper userProfileMapper;
|
|
|
-
|
|
|
@Autowired
|
|
|
private AcupointMapper acupointMapper;
|
|
|
|
|
|
- /**
|
|
|
- * 为指定子用户生成(或重新生成)全量穴位坐标
|
|
|
- *
|
|
|
- * @param profileId 子用户ID(user_profile.id)
|
|
|
- */
|
|
|
+ // ==================== 公开接口 ====================
|
|
|
+
|
|
|
@Override
|
|
|
@Transactional(rollbackFor = Exception.class)
|
|
|
public void generateForUser(Long profileId) {
|
|
|
- // 1. 校验子用户存在
|
|
|
UserProfile profile = userProfileMapper.selectById(profileId);
|
|
|
if (profile == null) {
|
|
|
throw new BusinessException(ResultCode.PROFILE_NOT_FOUND);
|
|
|
}
|
|
|
|
|
|
- // 2. 查询全量活跃穴位
|
|
|
LambdaQueryWrapper<Acupoint> wrapper = new LambdaQueryWrapper<>();
|
|
|
wrapper.eq(Acupoint::getStatus, 1);
|
|
|
List<Acupoint> acupoints = acupointMapper.selectList(wrapper);
|
|
|
|
|
|
- // 3. 清除该用户旧的穴位坐标记录
|
|
|
baseMapper.deleteByUserId(profileId);
|
|
|
|
|
|
- // 4. 计算并批量插入
|
|
|
List<UserAcupoint> records = buildAcupointRecords(profile, acupoints);
|
|
|
if (!records.isEmpty()) {
|
|
|
saveBatch(records);
|
|
|
}
|
|
|
|
|
|
- // 5. 更新 user_profile.acupoint_table_id(使用 profileId 标记已生成)
|
|
|
UserProfile update = new UserProfile();
|
|
|
update.setId(profileId);
|
|
|
update.setAcupointTableId(profileId);
|
|
|
userProfileMapper.updateById(update);
|
|
|
|
|
|
LogUtil.info(UserAcupointServiceImpl.class,
|
|
|
- "为子用户[{}]生成穴位坐标,共生成[{}]条记录", profileId, records.size());
|
|
|
+ "为子用户[{}]生成穴位坐标(算法3.0),共[{}]条记录,置信度[{}]",
|
|
|
+ profileId, records.size(), determineConfidence(profile));
|
|
|
}
|
|
|
|
|
|
- /**
|
|
|
- * 当穴位管理新增一条穴位时,为所有已生成过穴位表的子用户追加该穴位坐标
|
|
|
- *
|
|
|
- * @param acupointId 新增的穴位ID
|
|
|
- */
|
|
|
@Override
|
|
|
@Transactional(rollbackFor = Exception.class)
|
|
|
public void generateForAllUsersByAcupoint(Long acupointId) {
|
|
|
- // 1. 查询新增穴位信息
|
|
|
Acupoint acupoint = acupointMapper.selectById(acupointId);
|
|
|
if (acupoint == null) {
|
|
|
LogUtil.info(UserAcupointServiceImpl.class, "穴位[{}]不存在,跳过批量生成", acupointId);
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- // 2. 查询所有已生成过穴位表的子用户(acupoint_table_id 不为空)
|
|
|
LambdaQueryWrapper<UserProfile> profileWrapper = new LambdaQueryWrapper<>();
|
|
|
profileWrapper.isNotNull(UserProfile::getAcupointTableId);
|
|
|
List<UserProfile> profiles = userProfileMapper.selectList(profileWrapper);
|
|
|
|
|
|
int total = 0;
|
|
|
for (UserProfile profile : profiles) {
|
|
|
- // 3. 删除该用户下此穴位的旧记录(幂等处理)
|
|
|
baseMapper.deleteByUserIdAndAcupointId(profile.getId(), acupointId);
|
|
|
- // 4. 为该用户计算并插入新穴位坐标
|
|
|
List<UserAcupoint> records = buildAcupointRecords(profile, Collections.singletonList(acupoint));
|
|
|
if (!records.isEmpty()) {
|
|
|
saveBatch(records);
|
|
|
@@ -120,51 +135,225 @@ public class UserAcupointServiceImpl extends ServiceImpl<UserAcupointMapper, Use
|
|
|
acupointId, profiles.size(), total);
|
|
|
}
|
|
|
|
|
|
- /**
|
|
|
- * 查询指定子用户的穴位坐标列表
|
|
|
- *
|
|
|
- * @param profileId 子用户ID
|
|
|
- * @return 穴位坐标列表
|
|
|
- */
|
|
|
@Override
|
|
|
public List<UserAcupoint> listByUser(Long profileId) {
|
|
|
return baseMapper.listByUserId(profileId);
|
|
|
}
|
|
|
|
|
|
- // ======================== 核心算法 ========================
|
|
|
+ // ==================== 核心算法(3.0) ====================
|
|
|
|
|
|
/**
|
|
|
- * 为单个子用户批量构建穴位坐标记录列表
|
|
|
- *
|
|
|
- * @param profile 子用户体型数据
|
|
|
- * @param acupoints 需要计算的穴位列表
|
|
|
- * @return 待插入的 UserAcupoint 记录列表
|
|
|
+ * 为单个子用户批量构建穴位坐标记录
|
|
|
+ * 先预算全局参数(最终指寸、背部一寸宽度、置信度),再逐穴位计算坐标
|
|
|
*/
|
|
|
private List<UserAcupoint> buildAcupointRecords(UserProfile profile, List<Acupoint> acupoints) {
|
|
|
+ boolean hasRouteA = hasRouteA(profile);
|
|
|
+ BigDecimal finalCunMm = calculateFinalCunMm(profile);
|
|
|
+ BigDecimal backOneCunWidth = calculateBackOneCunWidth(profile, finalCunMm, hasRouteA);
|
|
|
+ String confidence = determineConfidence(profile);
|
|
|
+
|
|
|
List<UserAcupoint> records = new ArrayList<>();
|
|
|
for (Acupoint acupoint : acupoints) {
|
|
|
if (acupoint.getLocationType() == null) {
|
|
|
continue;
|
|
|
}
|
|
|
if (acupoint.getLocationType() == 2) {
|
|
|
- // 绝对定位:直接使用 abs_x/abs_y
|
|
|
- records.add(buildAbsoluteRecord(profile.getId(), acupoint));
|
|
|
+ records.add(buildAbsoluteRecord(profile.getId(), acupoint, confidence));
|
|
|
} else {
|
|
|
- // 相对定位:按指寸换算坐标
|
|
|
- records.addAll(buildRelativeRecords(profile, acupoint));
|
|
|
+ records.addAll(buildRelativeRecords(profile, acupoint, finalCunMm, backOneCunWidth, confidence));
|
|
|
}
|
|
|
}
|
|
|
return records;
|
|
|
}
|
|
|
|
|
|
+ // -------------------- 第一步:三路指寸融合 --------------------
|
|
|
+
|
|
|
+ private boolean hasRouteA(UserProfile profile) {
|
|
|
+ return profile.getFingerWidth1() != null
|
|
|
+ || profile.getFingerWidth15() != null
|
|
|
+ || profile.getFingerWidth3() != null;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * A路:实测融合值
|
|
|
+ * 1寸值权重50%,1.5寸值(折算为1寸)权重25%,3寸值(折算为1寸)权重25%
|
|
|
+ * 返回 null 表示无实测数据
|
|
|
+ */
|
|
|
+ private BigDecimal calculateRouteA(UserProfile profile) {
|
|
|
+ BigDecimal totalWeight = BigDecimal.ZERO;
|
|
|
+ BigDecimal weightedSum = BigDecimal.ZERO;
|
|
|
+
|
|
|
+ if (profile.getFingerWidth1() != null) {
|
|
|
+ BigDecimal valueMm = profile.getFingerWidth1().multiply(BigDecimal.TEN);
|
|
|
+ weightedSum = weightedSum.add(valueMm.multiply(BD_50));
|
|
|
+ totalWeight = totalWeight.add(BD_50);
|
|
|
+ }
|
|
|
+ if (profile.getFingerWidth15() != null) {
|
|
|
+ BigDecimal valueMm = profile.getFingerWidth15()
|
|
|
+ .divide(BD_1_5, 4, RoundingMode.HALF_UP)
|
|
|
+ .multiply(BigDecimal.TEN);
|
|
|
+ weightedSum = weightedSum.add(valueMm.multiply(BD_25));
|
|
|
+ totalWeight = totalWeight.add(BD_25);
|
|
|
+ }
|
|
|
+ if (profile.getFingerWidth3() != null) {
|
|
|
+ BigDecimal valueMm = profile.getFingerWidth3()
|
|
|
+ .divide(BD_3, 4, RoundingMode.HALF_UP)
|
|
|
+ .multiply(BigDecimal.TEN);
|
|
|
+ weightedSum = weightedSum.add(valueMm.multiply(BD_25));
|
|
|
+ totalWeight = totalWeight.add(BD_25);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (totalWeight.compareTo(BigDecimal.ZERO) == 0) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ return weightedSum.divide(totalWeight, 2, RoundingMode.HALF_UP);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * B路:BMI换算法
|
|
|
+ * B路指寸(mm) = 24.0 × (用户BMI / 标准人BMI)
|
|
|
+ * 返回 null 表示身高体重数据不可用
|
|
|
+ */
|
|
|
+ private BigDecimal calculateRouteB(UserProfile profile) {
|
|
|
+ if (profile.getHeight() == null || profile.getWeight() == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ if (profile.getHeight().compareTo(BigDecimal.ZERO) <= 0
|
|
|
+ || profile.getWeight().compareTo(BigDecimal.ZERO) <= 0) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ BigDecimal heightM = profile.getHeight().divide(BD_100, 4, RoundingMode.HALF_UP);
|
|
|
+ BigDecimal userBMI = profile.getWeight().divide(
|
|
|
+ heightM.multiply(heightM), 4, RoundingMode.HALF_UP);
|
|
|
+ BigDecimal bmiRatio = userBMI.divide(STANDARD_BMI, 4, RoundingMode.HALF_UP);
|
|
|
+ return STANDARD_CUN_MM.multiply(bmiRatio).setScale(2, RoundingMode.HALF_UP);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 最终指寸(mm) = 双路平均 > 单路 > 默认24mm
|
|
|
+ */
|
|
|
+ private BigDecimal calculateFinalCunMm(UserProfile profile) {
|
|
|
+ BigDecimal routeA = calculateRouteA(profile);
|
|
|
+ BigDecimal routeB = calculateRouteB(profile);
|
|
|
+
|
|
|
+ if (routeA != null && routeB != null) {
|
|
|
+ return routeA.add(routeB).divide(TWO, 2, RoundingMode.HALF_UP);
|
|
|
+ } else if (routeA != null) {
|
|
|
+ return routeA;
|
|
|
+ } else if (routeB != null) {
|
|
|
+ return routeB;
|
|
|
+ }
|
|
|
+ return STANDARD_CUN_MM;
|
|
|
+ }
|
|
|
+
|
|
|
+ // -------------------- 第二步:背部一寸宽度(双源融合) --------------------
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 来源一:肩宽推算 = 肩宽(mm) × 0.420 / 6
|
|
|
+ * 来源二:指寸验证 = 最终指寸(mm)(体型修正系数微调简化为0)
|
|
|
+ * 融合策略:差异率≤15%取平均;否则有实测取指寸,无实测取肩宽
|
|
|
+ */
|
|
|
+ private BigDecimal calculateBackOneCunWidth(UserProfile profile, BigDecimal finalCunMm, boolean hasRouteA) {
|
|
|
+ BigDecimal backCunShoulder = null;
|
|
|
+
|
|
|
+ if (profile.getShoulderWidth() != null
|
|
|
+ && profile.getShoulderWidth().compareTo(BigDecimal.ZERO) > 0) {
|
|
|
+ BigDecimal shoulderMm = profile.getShoulderWidth().multiply(BigDecimal.TEN);
|
|
|
+ BigDecimal scapulaDistance = shoulderMm.multiply(SCAPULA_RATIO);
|
|
|
+ backCunShoulder = scapulaDistance.divide(SCAPULA_CUN_COUNT, 2, RoundingMode.HALF_UP);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (backCunShoulder != null) {
|
|
|
+ BigDecimal diff = backCunShoulder.subtract(finalCunMm).abs();
|
|
|
+ BigDecimal diffRate = diff.divide(backCunShoulder, 4, RoundingMode.HALF_UP);
|
|
|
+
|
|
|
+ if (diffRate.compareTo(DIFF_THRESHOLD) <= 0) {
|
|
|
+ return backCunShoulder.add(finalCunMm).divide(TWO, 2, RoundingMode.HALF_UP);
|
|
|
+ } else if (hasRouteA) {
|
|
|
+ return finalCunMm;
|
|
|
+ } else {
|
|
|
+ return backCunShoulder;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return finalCunMm;
|
|
|
+ }
|
|
|
+
|
|
|
+ // -------------------- 第三步:置信度 --------------------
|
|
|
+
|
|
|
+ private String determineConfidence(UserProfile profile) {
|
|
|
+ boolean a = hasRouteA(profile);
|
|
|
+ boolean b = profile.getHeight() != null && profile.getWeight() != null;
|
|
|
+ if (a && b) return "high";
|
|
|
+ if (a || b) return "medium";
|
|
|
+ return "low";
|
|
|
+ }
|
|
|
+
|
|
|
+ // -------------------- 第四步:Y轴坐标 --------------------
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 优先使用 Panjabi归一化: Y = 脊柱长度(mm) × normalizedY
|
|
|
+ * 回退使用 寸制: Y = longitudeOffset × finalCunMm
|
|
|
+ */
|
|
|
+ private BigDecimal calculateY(UserProfile profile, Acupoint acupoint, BigDecimal finalCunMm) {
|
|
|
+ if (acupoint.getNormalizedY() != null && profile.getSpineLength() != null
|
|
|
+ && profile.getSpineLength().compareTo(BigDecimal.ZERO) > 0) {
|
|
|
+ BigDecimal spineLengthMm = profile.getSpineLength().multiply(BigDecimal.TEN);
|
|
|
+ return spineLengthMm.multiply(acupoint.getNormalizedY()).setScale(2, RoundingMode.HALF_UP);
|
|
|
+ }
|
|
|
+ if (acupoint.getLongitudeOffset() != null) {
|
|
|
+ return acupoint.getLongitudeOffset().multiply(finalCunMm).setScale(2, RoundingMode.HALF_UP);
|
|
|
+ }
|
|
|
+ return BigDecimal.ZERO;
|
|
|
+ }
|
|
|
+
|
|
|
+ // -------------------- 第五步:X轴坐标 --------------------
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 横向坐标计算(分策略):
|
|
|
+ * - 督脉/中心穴位: X=0
|
|
|
+ * - 膀胱经第一/二侧线: lateralOffset × backOneCunWidth × 区域系数
|
|
|
+ * - 八髎穴: 骶后孔基准偏移 × 身高缩放比例
|
|
|
+ */
|
|
|
+ private BigDecimal calculateLateralMm(Acupoint acupoint, BigDecimal backOneCunWidth, UserProfile profile) {
|
|
|
+ String code = acupoint.getAcupointCode();
|
|
|
+ if (code != null && BALIAO_OFFSETS.containsKey(code)) {
|
|
|
+ return calculateBaliaoX(code, profile);
|
|
|
+ }
|
|
|
+
|
|
|
+ BigDecimal lateralOffset = acupoint.getLateralOffset();
|
|
|
+ if (lateralOffset == null || lateralOffset.compareTo(BigDecimal.ZERO) == 0) {
|
|
|
+ return BigDecimal.ZERO;
|
|
|
+ }
|
|
|
+
|
|
|
+ BigDecimal regionCoefficient = BigDecimal.ONE;
|
|
|
+ if (acupoint.getNormalizedY() != null
|
|
|
+ && acupoint.getNormalizedY().compareTo(SACRAL_Y_THRESHOLD) >= 0) {
|
|
|
+ regionCoefficient = SACRAL_REGION_COEFFICIENT;
|
|
|
+ }
|
|
|
+
|
|
|
+ return lateralOffset.multiply(backOneCunWidth)
|
|
|
+ .multiply(regionCoefficient)
|
|
|
+ .setScale(2, RoundingMode.HALF_UP);
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
- * 绝对定位穴位:直接使用 abs_x/abs_y 构建一条坐标记录
|
|
|
- *
|
|
|
- * @param profileId 子用户ID
|
|
|
- * @param acupoint 穴位信息
|
|
|
- * @return 坐标记录
|
|
|
+ * 八髎穴X轴: 基准偏移(mm) × (用户身高 / 160cm标准身高)
|
|
|
*/
|
|
|
- private UserAcupoint buildAbsoluteRecord(Long profileId, Acupoint acupoint) {
|
|
|
+ private BigDecimal calculateBaliaoX(String acupointCode, UserProfile profile) {
|
|
|
+ BigDecimal baseOffset = BALIAO_OFFSETS.get(acupointCode);
|
|
|
+ if (baseOffset == null) {
|
|
|
+ return BigDecimal.ZERO;
|
|
|
+ }
|
|
|
+ BigDecimal heightScale = BigDecimal.ONE;
|
|
|
+ if (profile.getHeight() != null && profile.getHeight().compareTo(BigDecimal.ZERO) > 0) {
|
|
|
+ heightScale = profile.getHeight().divide(STANDARD_HEIGHT_CM, 4, RoundingMode.HALF_UP);
|
|
|
+ }
|
|
|
+ return baseOffset.multiply(heightScale).setScale(2, RoundingMode.HALF_UP);
|
|
|
+ }
|
|
|
+
|
|
|
+ // -------------------- 记录构建 --------------------
|
|
|
+
|
|
|
+ private UserAcupoint buildAbsoluteRecord(Long profileId, Acupoint acupoint, String confidence) {
|
|
|
UserAcupoint record = new UserAcupoint();
|
|
|
record.setUserId(profileId);
|
|
|
record.setAcupointId(acupoint.getId());
|
|
|
@@ -174,147 +363,56 @@ public class UserAcupointServiceImpl extends ServiceImpl<UserAcupointMapper, Use
|
|
|
record.setCunType(null);
|
|
|
record.setCoordinateX(acupoint.getAbsX() != null ? acupoint.getAbsX() : BigDecimal.ZERO);
|
|
|
record.setCoordinateY(acupoint.getAbsY() != null ? acupoint.getAbsY() : BigDecimal.ZERO);
|
|
|
+ record.setCoordinateZ(BigDecimal.ZERO);
|
|
|
+ record.setConfidence(confidence);
|
|
|
return record;
|
|
|
}
|
|
|
|
|
|
- /**
|
|
|
- * 相对定位穴位:根据子用户体型数据计算坐标,按侧别生成 1~2 条记录
|
|
|
- *
|
|
|
- * @param profile 子用户体型数据
|
|
|
- * @param acupoint 穴位信息
|
|
|
- * @return 坐标记录列表(中心1条;单侧1条;双侧2条)
|
|
|
- */
|
|
|
- private List<UserAcupoint> buildRelativeRecords(UserProfile profile, Acupoint acupoint) {
|
|
|
+ private List<UserAcupoint> buildRelativeRecords(UserProfile profile, Acupoint acupoint,
|
|
|
+ BigDecimal finalCunMm, BigDecimal backOneCunWidth,
|
|
|
+ String confidence) {
|
|
|
List<UserAcupoint> list = new ArrayList<>();
|
|
|
|
|
|
- // 获取 1寸 对应的 mm 换算值
|
|
|
- BigDecimal oneCunMm = getOneCunMm(profile, acupoint.getFingerWidthType());
|
|
|
- // 纵向坐标(Y):纵向偏移 × 1寸换算值
|
|
|
- BigDecimal coordY = acupoint.getLongitudeOffset() != null
|
|
|
- ? acupoint.getLongitudeOffset().multiply(oneCunMm).setScale(2, RoundingMode.HALF_UP)
|
|
|
- : BigDecimal.ZERO;
|
|
|
- // 横向偏移值(用于左/右侧计算)
|
|
|
- BigDecimal lateralMm = acupoint.getLateralOffset() != null
|
|
|
- ? acupoint.getLateralOffset().multiply(oneCunMm).setScale(2, RoundingMode.HALF_UP)
|
|
|
- : BigDecimal.ZERO;
|
|
|
-
|
|
|
- // 指寸类型文本
|
|
|
- String cunTypeText = resolveCunType(acupoint.getFingerWidthType());
|
|
|
+ BigDecimal coordY = calculateY(profile, acupoint, finalCunMm);
|
|
|
+ BigDecimal lateralMm = calculateLateralMm(acupoint, backOneCunWidth, profile);
|
|
|
|
|
|
Integer side = acupoint.getSide();
|
|
|
if (side == null || side == 1) {
|
|
|
- // 中心穴位,X=0
|
|
|
- list.add(buildRecord(profile.getId(), acupoint, "center",
|
|
|
- acupoint.getLongitudeOffset(), cunTypeText, BigDecimal.ZERO, coordY));
|
|
|
+ list.add(buildRecord(profileId(profile), acupoint, "center",
|
|
|
+ BigDecimal.ZERO, coordY, confidence));
|
|
|
} else if (side == 2) {
|
|
|
- // 仅左侧,X 为负数(向左)
|
|
|
- list.add(buildRecord(profile.getId(), acupoint, "left",
|
|
|
- acupoint.getLongitudeOffset(), cunTypeText, lateralMm.negate(), coordY));
|
|
|
+ list.add(buildRecord(profileId(profile), acupoint, "left",
|
|
|
+ lateralMm.negate(), coordY, confidence));
|
|
|
} else if (side == 3) {
|
|
|
- // 仅右侧,X 为正数(向右)
|
|
|
- list.add(buildRecord(profile.getId(), acupoint, "right",
|
|
|
- acupoint.getLongitudeOffset(), cunTypeText, lateralMm, coordY));
|
|
|
+ list.add(buildRecord(profileId(profile), acupoint, "right",
|
|
|
+ lateralMm, coordY, confidence));
|
|
|
} else if (side == 4) {
|
|
|
- // 双侧:生成左右两条记录
|
|
|
- list.add(buildRecord(profile.getId(), acupoint, "left",
|
|
|
- acupoint.getLongitudeOffset(), cunTypeText, lateralMm.negate(), coordY));
|
|
|
- list.add(buildRecord(profile.getId(), acupoint, "right",
|
|
|
- acupoint.getLongitudeOffset(), cunTypeText, lateralMm, coordY));
|
|
|
+ list.add(buildRecord(profileId(profile), acupoint, "left",
|
|
|
+ lateralMm.negate(), coordY, confidence));
|
|
|
+ list.add(buildRecord(profileId(profile), acupoint, "right",
|
|
|
+ lateralMm, coordY, confidence));
|
|
|
}
|
|
|
return list;
|
|
|
}
|
|
|
|
|
|
- /**
|
|
|
- * 构建单条 UserAcupoint 记录
|
|
|
- *
|
|
|
- * @param profileId 子用户ID
|
|
|
- * @param acupoint 穴位信息
|
|
|
- * @param side 侧别(center/left/right)
|
|
|
- * @param cunValue 纵向偏移(寸)
|
|
|
- * @param cunType 指寸类型文本
|
|
|
- * @param coordinateX X 坐标(mm)
|
|
|
- * @param coordinateY Y 坐标(mm)
|
|
|
- * @return 坐标记录
|
|
|
- */
|
|
|
private UserAcupoint buildRecord(Long profileId, Acupoint acupoint, String side,
|
|
|
- BigDecimal cunValue, String cunType,
|
|
|
- BigDecimal coordinateX, BigDecimal coordinateY) {
|
|
|
+ BigDecimal coordinateX, BigDecimal coordinateY,
|
|
|
+ String confidence) {
|
|
|
UserAcupoint record = new UserAcupoint();
|
|
|
record.setUserId(profileId);
|
|
|
record.setAcupointId(acupoint.getId());
|
|
|
record.setAcupointName(acupoint.getName());
|
|
|
record.setAcupointSide(side);
|
|
|
- record.setCunValue(cunValue);
|
|
|
- record.setCunType(cunType);
|
|
|
+ record.setCunValue(acupoint.getLongitudeOffset());
|
|
|
+ record.setCunType(null);
|
|
|
record.setCoordinateX(coordinateX);
|
|
|
record.setCoordinateY(coordinateY);
|
|
|
+ record.setCoordinateZ(BigDecimal.ZERO);
|
|
|
+ record.setConfidence(confidence);
|
|
|
return record;
|
|
|
}
|
|
|
|
|
|
- /**
|
|
|
- * 根据穴位的指寸类型和子用户的实测指宽,计算 1寸 对应的 mm 换算值
|
|
|
- * <ul>
|
|
|
- * <li>指寸类型=1(1寸):1寸 = fingerWidth1 × 10 mm</li>
|
|
|
- * <li>指寸类型=2(1.5寸):1寸 = (fingerWidth15 / 1.5) × 10 mm</li>
|
|
|
- * <li>指寸类型=3(3寸):1寸 = (fingerWidth3 / 3) × 10 mm</li>
|
|
|
- * <li>如未设置指宽则使用默认值 18 mm/寸</li>
|
|
|
- * </ul>
|
|
|
- *
|
|
|
- * @param profile 子用户体型数据
|
|
|
- * @param fingerWidthType 穴位指寸类型(1/2/3)
|
|
|
- * @return 1寸对应的 mm 值
|
|
|
- */
|
|
|
- private BigDecimal getOneCunMm(UserProfile profile, Integer fingerWidthType) {
|
|
|
- if (fingerWidthType == null) {
|
|
|
- return DEFAULT_ONE_CUN_MM;
|
|
|
- }
|
|
|
- switch (fingerWidthType) {
|
|
|
- case 1:
|
|
|
- // 1寸指宽(cm)× 10 = mm
|
|
|
- if (profile.getFingerWidth1() != null) {
|
|
|
- return profile.getFingerWidth1().multiply(BigDecimal.TEN).setScale(2, RoundingMode.HALF_UP);
|
|
|
- }
|
|
|
- break;
|
|
|
- case 2:
|
|
|
- // 1.5寸指宽 / 1.5 × 10 = 1寸 mm
|
|
|
- if (profile.getFingerWidth15() != null) {
|
|
|
- return profile.getFingerWidth15()
|
|
|
- .divide(new BigDecimal("1.5"), 4, RoundingMode.HALF_UP)
|
|
|
- .multiply(BigDecimal.TEN)
|
|
|
- .setScale(2, RoundingMode.HALF_UP);
|
|
|
- }
|
|
|
- break;
|
|
|
- case 3:
|
|
|
- // 3寸指宽 / 3 × 10 = 1寸 mm
|
|
|
- if (profile.getFingerWidth3() != null) {
|
|
|
- return profile.getFingerWidth3()
|
|
|
- .divide(new BigDecimal("3"), 4, RoundingMode.HALF_UP)
|
|
|
- .multiply(BigDecimal.TEN)
|
|
|
- .setScale(2, RoundingMode.HALF_UP);
|
|
|
- }
|
|
|
- break;
|
|
|
- default:
|
|
|
- break;
|
|
|
- }
|
|
|
- // 指宽未设置,使用默认值
|
|
|
- return DEFAULT_ONE_CUN_MM;
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 将穴位的指寸类型编号转为中文文本
|
|
|
- *
|
|
|
- * @param fingerWidthType 指寸类型编号(1/2/3)
|
|
|
- * @return 中文类型文本(1寸/1.5寸/3寸)
|
|
|
- */
|
|
|
- private String resolveCunType(Integer fingerWidthType) {
|
|
|
- if (fingerWidthType == null) {
|
|
|
- return null;
|
|
|
- }
|
|
|
- switch (fingerWidthType) {
|
|
|
- case 1: return "1寸";
|
|
|
- case 2: return "1.5寸";
|
|
|
- case 3: return "3寸";
|
|
|
- default: return null;
|
|
|
- }
|
|
|
+ private Long profileId(UserProfile profile) {
|
|
|
+ return profile.getId();
|
|
|
}
|
|
|
}
|