|
|
@@ -0,0 +1,229 @@
|
|
|
+package com.etotem.cfc.unit;
|
|
|
+
|
|
|
+import com.etotem.cfc.service.FamilyMemberDimensionBaseScoreService;
|
|
|
+import org.junit.jupiter.api.Test;
|
|
|
+import org.junit.jupiter.api.extension.ExtendWith;
|
|
|
+import org.mockito.InjectMocks;
|
|
|
+import org.mockito.junit.jupiter.MockitoExtension;
|
|
|
+
|
|
|
+import java.util.HashMap;
|
|
|
+import java.util.Map;
|
|
|
+
|
|
|
+import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
|
+import static org.junit.jupiter.api.Assertions.assertNotNull;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 五维基础能量分算法单测。
|
|
|
+ *
|
|
|
+ * <p>设计文档 docs/superpowers/specs/2026-09-09-tianpan-tradition-design.md §3.1/§5.1
|
|
|
+ * 要求为五维基础分计算(calcBaseScores)提供单测,覆盖:全数据示例(§2.6)、
|
|
|
+ * 各层缺数据兜底(wuxing / westernSign / lifeNumber 为 null)、主数化简(11/22/33)、
|
|
|
+ * 星座边界(12 星座全覆盖 + 未知星座字符串)。</p>
|
|
|
+ *
|
|
|
+ * <p>实现说明:设计文档原定该算法位于 {@code FiveDimensionScoreService},实际实现时
|
|
|
+ * 五维基础分算法落在 {@link FamilyMemberDimensionBaseScoreService}(含 calcBaseScores /
|
|
|
+ * calcAndSave / getScores),而 {@code FiveDimensionScoreService} 仅承担五维快照记录。
|
|
|
+ * 因此本测试以实际实现类 {@link FamilyMemberDimensionBaseScoreService} 为被测对象,
|
|
|
+ * 用例口径严格对齐设计文档。</p>
|
|
|
+ */
|
|
|
+@ExtendWith(MockitoExtension.class)
|
|
|
+class FiveDimensionScoreServiceTest {
|
|
|
+
|
|
|
+ @InjectMocks
|
|
|
+ private FamilyMemberDimensionBaseScoreService service;
|
|
|
+
|
|
|
+ /** 构造完整五行 map(量纲 0-100)。 */
|
|
|
+ private Map<String, Integer> wuxing(int wood, int fire, int earth, int metal, int water) {
|
|
|
+ Map<String, Integer> m = new HashMap<>();
|
|
|
+ m.put("wood", wood);
|
|
|
+ m.put("fire", fire);
|
|
|
+ m.put("earth", earth);
|
|
|
+ m.put("metal", metal);
|
|
|
+ m.put("water", water);
|
|
|
+ return m;
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 构造五行值全 50 的 map。 */
|
|
|
+ private Map<String, Integer> wuxingNeutral() {
|
|
|
+ return wuxing(50, 50, 50, 50, 50);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 全数据用例:严格对照设计文档 §2.6 计算示例。
|
|
|
+ *
|
|
|
+ * <p>输入:八字五行 {wood:40, fire:60, earth:50, metal:30, water:20}、
|
|
|
+ * 星座狮子座(火象→心 mind)、灵数 7(→智 wisdom)。</p>
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ void calcBaseScores_fullData_matchesDocExample26() {
|
|
|
+ Map<String, Integer> result = service.calcBaseScores(
|
|
|
+ wuxing(40, 60, 50, 30, 20), "狮子座", 7);
|
|
|
+
|
|
|
+ assertEquals(30, result.get("body")); // earth 50×0.6=30
|
|
|
+ assertEquals(56, result.get("mind")); // fire 60×0.6=36 + 狮子座(火→心)20 = 56
|
|
|
+ assertEquals(38, result.get("wisdom")); // metal 30×0.6=18 + 灵数7(→智)20 = 38
|
|
|
+ assertEquals(24, result.get("action")); // wood 40×0.6=24
|
|
|
+ assertEquals(12, result.get("wealth")); // water 20×0.6=12
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 全部为 null:wuxing / westernSign / lifeNumber 均为 null 时,
|
|
|
+ * 五维全部取中性值 50(设计文档 §2.3 兜底口径;§5.1 中「50×0.6=30」为笔误)。
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ void calcBaseScores_allNull_returnsNeutral50() {
|
|
|
+ Map<String, Integer> result = service.calcBaseScores(null, null, null);
|
|
|
+
|
|
|
+ assertNotNull(result);
|
|
|
+ assertEquals(5, result.size());
|
|
|
+ assertEquals(50, result.get("body"));
|
|
|
+ assertEquals(50, result.get("mind"));
|
|
|
+ assertEquals(50, result.get("wisdom"));
|
|
|
+ assertEquals(50, result.get("action"));
|
|
|
+ assertEquals(50, result.get("wealth"));
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 兜底:wuxing=null 但星座/灵数有值 —— 基础 50 上叠加星座/灵数命中。 */
|
|
|
+ @Test
|
|
|
+ void calcBaseScores_nullWuxing_withZodiacAndLifeNumber() {
|
|
|
+ Map<String, Integer> result = service.calcBaseScores(null, "狮子座", 7);
|
|
|
+
|
|
|
+ assertEquals(50, result.get("body"));
|
|
|
+ assertEquals(70, result.get("mind")); // 50 + 狮子座(→心)20
|
|
|
+ assertEquals(70, result.get("wisdom")); // 50 + 灵数7(→智)20
|
|
|
+ assertEquals(50, result.get("action"));
|
|
|
+ assertEquals(50, result.get("wealth"));
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 兜底:westernSign=null 且 lifeNumber=null,仅八字层有效。 */
|
|
|
+ @Test
|
|
|
+ void calcBaseScores_nullWesternSignAndLifeNumber_onlyWuxing() {
|
|
|
+ Map<String, Integer> result = service.calcBaseScores(
|
|
|
+ wuxing(40, 60, 50, 30, 20), null, null);
|
|
|
+
|
|
|
+ assertEquals(30, result.get("body"));
|
|
|
+ assertEquals(36, result.get("mind"));
|
|
|
+ assertEquals(18, result.get("wisdom"));
|
|
|
+ assertEquals(24, result.get("action"));
|
|
|
+ assertEquals(12, result.get("wealth"));
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 兜底:lifeNumber=null,仅八字层 + 星座层有效。 */
|
|
|
+ @Test
|
|
|
+ void calcBaseScores_nullLifeNumber_onlyWuxingAndZodiac() {
|
|
|
+ Map<String, Integer> result = service.calcBaseScores(
|
|
|
+ wuxing(40, 60, 50, 30, 20), "狮子座", null);
|
|
|
+
|
|
|
+ assertEquals(30, result.get("body"));
|
|
|
+ assertEquals(56, result.get("mind")); // 36 + 狮子座20
|
|
|
+ assertEquals(18, result.get("wisdom"));
|
|
|
+ assertEquals(24, result.get("action"));
|
|
|
+ assertEquals(12, result.get("wealth"));
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 兜底:wuxing map 缺失某五行键时,该维度取中性值 50×0.6=30。 */
|
|
|
+ @Test
|
|
|
+ void calcBaseScores_missingWuxingElement_usesNeutral50() {
|
|
|
+ Map<String, Integer> partial = new HashMap<>();
|
|
|
+ partial.put("fire", 60);
|
|
|
+ partial.put("earth", 50);
|
|
|
+ partial.put("metal", 30);
|
|
|
+ partial.put("water", 20);
|
|
|
+ // 缺 wood → action 取中性 50×0.6=30
|
|
|
+
|
|
|
+ Map<String, Integer> result = service.calcBaseScores(partial, null, null);
|
|
|
+
|
|
|
+ assertEquals(30, result.get("action")); // wood 缺失 → 50×0.6=30
|
|
|
+ assertEquals(36, result.get("mind"));
|
|
|
+ assertEquals(30, result.get("body"));
|
|
|
+ assertEquals(18, result.get("wisdom"));
|
|
|
+ assertEquals(12, result.get("wealth"));
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 主数化简:11→2→心(mind)。 */
|
|
|
+ @Test
|
|
|
+ void calcBaseScores_masterNumber_11MapsTo2() {
|
|
|
+ Map<String, Integer> result = service.calcBaseScores(wuxingNeutral(), null, 11);
|
|
|
+
|
|
|
+ assertEquals(50, result.get("mind")); // 30 + 灵数11(→2→心)20
|
|
|
+ assertEquals(30, result.get("body"));
|
|
|
+ assertEquals(30, result.get("wisdom"));
|
|
|
+ assertEquals(30, result.get("action"));
|
|
|
+ assertEquals(30, result.get("wealth"));
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 主数化简:22→4→身(body)。 */
|
|
|
+ @Test
|
|
|
+ void calcBaseScores_masterNumber_22MapsTo4() {
|
|
|
+ Map<String, Integer> result = service.calcBaseScores(wuxingNeutral(), null, 22);
|
|
|
+
|
|
|
+ assertEquals(50, result.get("body")); // 30 + 灵数22(→4→身)20
|
|
|
+ assertEquals(30, result.get("mind"));
|
|
|
+ assertEquals(30, result.get("wisdom"));
|
|
|
+ assertEquals(30, result.get("action"));
|
|
|
+ assertEquals(30, result.get("wealth"));
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 主数化简:33→6→心(mind)。 */
|
|
|
+ @Test
|
|
|
+ void calcBaseScores_masterNumber_33MapsTo6() {
|
|
|
+ Map<String, Integer> result = service.calcBaseScores(wuxingNeutral(), null, 33);
|
|
|
+
|
|
|
+ assertEquals(50, result.get("mind")); // 30 + 灵数33(→6→心)20
|
|
|
+ assertEquals(30, result.get("body"));
|
|
|
+ assertEquals(30, result.get("wisdom"));
|
|
|
+ assertEquals(30, result.get("action"));
|
|
|
+ assertEquals(30, result.get("wealth"));
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 非主数 >9 按 digit sum 递归化简(如 19→1→行 action)。 */
|
|
|
+ @Test
|
|
|
+ void calcBaseScores_nonMasterDigitSum_reduces() {
|
|
|
+ Map<String, Integer> result = service.calcBaseScores(wuxingNeutral(), null, 19);
|
|
|
+
|
|
|
+ // 19 → 1+9=10 → 1+0=1 → action
|
|
|
+ assertEquals(50, result.get("action")); // 30 + 灵数19(→1→行)20
|
|
|
+ assertEquals(30, result.get("mind"));
|
|
|
+ assertEquals(30, result.get("body"));
|
|
|
+ assertEquals(30, result.get("wisdom"));
|
|
|
+ assertEquals(30, result.get("wealth"));
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 星座边界:12 星座全部覆盖且不抛异常,并按四元素正确命中维度。 */
|
|
|
+ @Test
|
|
|
+ void calcBaseScores_all12ZodiacSigns() {
|
|
|
+ Map<String, Integer> w = wuxing(40, 60, 50, 30, 20);
|
|
|
+
|
|
|
+ // 火象(白羊/狮子/射手)→ 心 mind:36+20=56
|
|
|
+ assertEquals(56, service.calcBaseScores(w, "白羊座", null).get("mind"));
|
|
|
+ assertEquals(56, service.calcBaseScores(w, "狮子座", null).get("mind"));
|
|
|
+ assertEquals(56, service.calcBaseScores(w, "射手座", null).get("mind"));
|
|
|
+
|
|
|
+ // 土象(金牛/处女/摩羯)→ 身 body:30+20=50
|
|
|
+ assertEquals(50, service.calcBaseScores(w, "金牛座", null).get("body"));
|
|
|
+ assertEquals(50, service.calcBaseScores(w, "处女座", null).get("body"));
|
|
|
+ assertEquals(50, service.calcBaseScores(w, "摩羯座", null).get("body"));
|
|
|
+
|
|
|
+ // 风象(双子/天秤/水瓶)→ 行 action:24+20=44
|
|
|
+ assertEquals(44, service.calcBaseScores(w, "双子座", null).get("action"));
|
|
|
+ assertEquals(44, service.calcBaseScores(w, "天秤座", null).get("action"));
|
|
|
+ assertEquals(44, service.calcBaseScores(w, "水瓶座", null).get("action"));
|
|
|
+
|
|
|
+ // 水象(巨蟹/天蝎/双鱼)→ 富 wealth:12+20=32
|
|
|
+ assertEquals(32, service.calcBaseScores(w, "巨蟹座", null).get("wealth"));
|
|
|
+ assertEquals(32, service.calcBaseScores(w, "天蝎座", null).get("wealth"));
|
|
|
+ assertEquals(32, service.calcBaseScores(w, "双鱼座", null).get("wealth"));
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 星座边界:未知星座字符串不命中任何维度(不抛异常、不加分)。 */
|
|
|
+ @Test
|
|
|
+ void calcBaseScores_unknownZodiacNoBonus() {
|
|
|
+ Map<String, Integer> result = service.calcBaseScores(wuxingNeutral(), "未知星座", null);
|
|
|
+
|
|
|
+ assertEquals(30, result.get("body"));
|
|
|
+ assertEquals(30, result.get("mind"));
|
|
|
+ assertEquals(30, result.get("wisdom"));
|
|
|
+ assertEquals(30, result.get("action"));
|
|
|
+ assertEquals(30, result.get("wealth"));
|
|
|
+ }
|
|
|
+}
|