ソースを参照

feat(v1.2): 心智模型重构 — DAN 70% + 先天 30% + 行为修正

Phase 0: 数据模型
- DanAssessmentResult: EMI 4 字段 (emotion_management/empathy/social_adaptability/self_motivation)
- User: +birthHour 出生时辰
- family_member_attributes: 新结构 (member_id/member_type/birth_datetime/eight_characters/wuxing_elements/mind_base_score/wisdom_base_score/behavior_modifier)
- schema.sql + DatabaseInitializer 全面更新

Phase 1: 心智计算
- FamilyMemberAttributeService: autoCalculate(生肖/八字四柱/五行星盘/生命灵数) + calcInnateScore
- FamilyAttributeController: /api/family/attributes/{update,get,calculate}
- EnergyService: calcChildMind=EMI 70%+先天30%+行为修正; calcChildWisdom=COG 70%+先天30%+行为修正
- FinanceCheckinService: +awardEnergy 接入

Phase 2-3: 前端
- user-edit.vue: +出生时辰选择器
- cognitive-report.vue: 认知测评报告页
- health_reports: child_id→user_id

已实现降级策略: DAN→先天→任务完成率
User 3 ヶ月 前
コミット
139cfbafcf
100 ファイル変更4126 行追加275 行削除
  1. 156 0
      cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java
  2. 3 2
      cfc-backend/src/main/java/com/etotem/cfc/config/JwtInterceptor.java
  3. 2 2
      cfc-backend/src/main/java/com/etotem/cfc/controller/HealthReportController.java
  4. 39 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/BaziConfigController.java
  5. 39 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/BloodTypeConfigController.java
  6. 54 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/FamilyAttributeController.java
  7. 39 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/ZodiacConfigController.java
  8. 38 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/BaziConfig.java
  9. 34 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/BloodTypeConfig.java
  10. 12 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/DanAssessmentResult.java
  11. 52 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/FamilyMemberAttributes.java
  12. 2 2
      cfc-backend/src/main/java/com/etotem/cfc/entity/HealthReport.java
  13. 3 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/User.java
  14. 44 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/ZodiacConfig.java
  15. 9 0
      cfc-backend/src/main/java/com/etotem/cfc/mapper/BaziConfigMapper.java
  16. 9 0
      cfc-backend/src/main/java/com/etotem/cfc/mapper/BloodTypeConfigMapper.java
  17. 9 0
      cfc-backend/src/main/java/com/etotem/cfc/mapper/FamilyMemberAttributesMapper.java
  18. 9 0
      cfc-backend/src/main/java/com/etotem/cfc/mapper/ZodiacConfigMapper.java
  19. 29 0
      cfc-backend/src/main/java/com/etotem/cfc/service/BaziConfigService.java
  20. 29 0
      cfc-backend/src/main/java/com/etotem/cfc/service/BloodTypeConfigService.java
  21. 1 1
      cfc-backend/src/main/java/com/etotem/cfc/service/ContentSectionService.java
  22. 148 11
      cfc-backend/src/main/java/com/etotem/cfc/service/EnergyService.java
  23. 431 0
      cfc-backend/src/main/java/com/etotem/cfc/service/FamilyMemberAttributeService.java
  24. 15 0
      cfc-backend/src/main/java/com/etotem/cfc/service/FinanceCheckinService.java
  25. 9 9
      cfc-backend/src/main/java/com/etotem/cfc/service/HealthReportService.java
  26. 29 0
      cfc-backend/src/main/java/com/etotem/cfc/service/ZodiacConfigService.java
  27. 75 1
      cfc-backend/src/main/resources/schema.sql
  28. 4 0
      cfc-frontend/pages.json
  29. 6 2
      cfc-frontend/pages/action/index.vue
  30. 6 2
      cfc-frontend/pages/body/index.vue
  31. 5 2
      cfc-frontend/pages/index/parent-index.vue
  32. 104 234
      cfc-frontend/pages/login/login.vue
  33. 14 2
      cfc-frontend/pages/mind/index.vue
  34. 17 0
      cfc-frontend/pages/user-edit/user-edit.vue
  35. 6 2
      cfc-frontend/pages/wealth/index.vue
  36. 131 0
      cfc-frontend/pages/wisdom/cognitive-report.vue
  37. 6 2
      cfc-frontend/pages/wisdom/index.vue
  38. 99 0
      cfc-web/src/api/admin.js
  39. 20 1
      cfc-web/src/router/index.js
  40. 12 0
      cfc-web/src/views/Layout.vue
  41. 160 0
      cfc-web/src/views/admin/BaziConfigs.vue
  42. 137 0
      cfc-web/src/views/admin/BloodTypeConfigs.vue
  43. 160 0
      cfc-web/src/views/admin/ZodiacConfigs.vue
  44. 57 0
      dan/danshop/.classpath
  45. 87 0
      dan/danshop/.factorypath
  46. 34 0
      dan/danshop/.project
  47. 4 0
      dan/danshop/.settings/org.eclipse.core.resources.prefs
  48. 4 0
      dan/danshop/.settings/org.eclipse.jdt.apt.core.prefs
  49. 10 0
      dan/danshop/.settings/org.eclipse.jdt.core.prefs
  50. 4 0
      dan/danshop/.settings/org.eclipse.m2e.core.prefs
  51. 142 0
      dan/danshop/pom.xml
  52. 16 0
      dan/danshop/src/main/java/com/etotem/danshop/DanShopApplication.java
  53. 72 0
      dan/danshop/src/main/java/com/etotem/danshop/common/DanShopConstants.java
  54. 50 0
      dan/danshop/src/main/java/com/etotem/danshop/common/Result.java
  55. 76 0
      dan/danshop/src/main/java/com/etotem/danshop/config/CfcBackendClient.java
  56. 18 0
      dan/danshop/src/main/java/com/etotem/danshop/config/MybatisPlusConfig.java
  57. 24 0
      dan/danshop/src/main/java/com/etotem/danshop/config/RedisConfig.java
  58. 20 0
      dan/danshop/src/main/java/com/etotem/danshop/config/WebMvcConfig.java
  59. 16 0
      dan/danshop/src/main/java/com/etotem/danshop/config/WechatPayConfig.java
  60. 20 0
      dan/danshop/src/main/java/com/etotem/danshop/controller/LogisticsController.java
  61. 45 0
      dan/danshop/src/main/java/com/etotem/danshop/controller/PaymentController.java
  62. 43 0
      dan/danshop/src/main/java/com/etotem/danshop/controller/admin/AdminCategoryController.java
  63. 33 0
      dan/danshop/src/main/java/com/etotem/danshop/controller/admin/AdminLogisticsController.java
  64. 47 0
      dan/danshop/src/main/java/com/etotem/danshop/controller/admin/AdminOrderController.java
  65. 54 0
      dan/danshop/src/main/java/com/etotem/danshop/controller/admin/AdminProductController.java
  66. 53 0
      dan/danshop/src/main/java/com/etotem/danshop/controller/admin/AdminProfitRuleController.java
  67. 46 0
      dan/danshop/src/main/java/com/etotem/danshop/controller/buyer/AddressController.java
  68. 42 0
      dan/danshop/src/main/java/com/etotem/danshop/controller/buyer/CartController.java
  69. 24 0
      dan/danshop/src/main/java/com/etotem/danshop/controller/buyer/CategoryController.java
  70. 59 0
      dan/danshop/src/main/java/com/etotem/danshop/controller/buyer/OrderController.java
  71. 40 0
      dan/danshop/src/main/java/com/etotem/danshop/controller/buyer/ProductController.java
  72. 44 0
      dan/danshop/src/main/java/com/etotem/danshop/controller/seller/SellerOrderController.java
  73. 51 0
      dan/danshop/src/main/java/com/etotem/danshop/controller/seller/SellerProductController.java
  74. 20 0
      dan/danshop/src/main/java/com/etotem/danshop/dto/AddressDTO.java
  75. 17 0
      dan/danshop/src/main/java/com/etotem/danshop/dto/CartItemDTO.java
  76. 13 0
      dan/danshop/src/main/java/com/etotem/danshop/dto/CategoryDTO.java
  77. 16 0
      dan/danshop/src/main/java/com/etotem/danshop/dto/LogisticsDTO.java
  78. 20 0
      dan/danshop/src/main/java/com/etotem/danshop/dto/OrderCreateDTO.java
  79. 26 0
      dan/danshop/src/main/java/com/etotem/danshop/dto/OrderDTO.java
  80. 17 0
      dan/danshop/src/main/java/com/etotem/danshop/dto/OrderItemDTO.java
  81. 14 0
      dan/danshop/src/main/java/com/etotem/danshop/dto/PayNotifyDTO.java
  82. 26 0
      dan/danshop/src/main/java/com/etotem/danshop/dto/ProductDTO.java
  83. 15 0
      dan/danshop/src/main/java/com/etotem/danshop/dto/ProfitRuleDTO.java
  84. 40 0
      dan/danshop/src/main/java/com/etotem/danshop/entity/Address.java
  85. 41 0
      dan/danshop/src/main/java/com/etotem/danshop/entity/Category.java
  86. 21 0
      dan/danshop/src/main/java/com/etotem/danshop/entity/Favorite.java
  87. 50 0
      dan/danshop/src/main/java/com/etotem/danshop/entity/Logistics.java
  88. 72 0
      dan/danshop/src/main/java/com/etotem/danshop/entity/Order.java
  89. 40 0
      dan/danshop/src/main/java/com/etotem/danshop/entity/OrderItem.java
  90. 27 0
      dan/danshop/src/main/java/com/etotem/danshop/entity/OrderLog.java
  91. 72 0
      dan/danshop/src/main/java/com/etotem/danshop/entity/Product.java
  92. 38 0
      dan/danshop/src/main/java/com/etotem/danshop/entity/ProfitLog.java
  93. 37 0
      dan/danshop/src/main/java/com/etotem/danshop/entity/ProfitRule.java
  94. 9 0
      dan/danshop/src/main/java/com/etotem/danshop/mapper/AddressMapper.java
  95. 9 0
      dan/danshop/src/main/java/com/etotem/danshop/mapper/CategoryMapper.java
  96. 9 0
      dan/danshop/src/main/java/com/etotem/danshop/mapper/FavoriteMapper.java
  97. 9 0
      dan/danshop/src/main/java/com/etotem/danshop/mapper/LogisticsMapper.java
  98. 9 0
      dan/danshop/src/main/java/com/etotem/danshop/mapper/OrderItemMapper.java
  99. 9 0
      dan/danshop/src/main/java/com/etotem/danshop/mapper/OrderLogMapper.java
  100. 9 0
      dan/danshop/src/main/java/com/etotem/danshop/mapper/OrderMapper.java

+ 156 - 0
cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java

@@ -1003,6 +1003,9 @@ log.info("已添加template_id列到tasks表");
             insertContentSectionSeed("mind", "daily_tip", "每日心理", "[\"anonymous\"]", 2);
             insertContentSectionSeed("mind", "recommended_reading", "推荐阅读", "[\"anonymous\"]", 3);
             insertContentSectionSeed("mind", "premium_content", "会员专属内容", "[\"parent\",\"teacher\"]", 4);
+            insertContentSectionSeed("mind", "mind_tasks", "心智任务", "[\"parent\",\"child\"]", 5);
+            insertContentSectionSeed("mind", "related_activities", "相关活动", "[\"parent\",\"child\"]", 6);
+            insertContentSectionSeed("mind", "purchased_courses", "已购课程", "[\"parent\",\"child\"]", 7);
             insertContentSectionSeed("body", "func_entries", "功能入口", "[\"anonymous\"]", 1);
             insertContentSectionSeed("body", "daily_stats", "今日数据", "[\"anonymous\"]", 2);
             insertContentSectionSeed("body", "health_tips", "健康小贴士", "[\"anonymous\"]", 3);
@@ -1460,6 +1463,85 @@ log.info("已添加template_id列到tasks表");
             log.warn("检查/添加 dan_assessment_results 认知维度字段失败: {}", e.getMessage());
         }
 
+        // 迁移: dan_assessment_results 表添加 EMI 维度字段(v1.2 心智模型重构)
+        try {
+            Integer colExists = jdbcTemplate.queryForObject(
+                "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'dan_assessment_results' AND COLUMN_NAME = 'emotion_management_score'",
+                Integer.class);
+            if (colExists == null || colExists == 0) {
+                jdbcTemplate.execute("ALTER TABLE dan_assessment_results " +
+                    "ADD COLUMN emotion_management_score INT COMMENT '情绪管理得分(EMI)', " +
+                    "ADD COLUMN empathy_score INT COMMENT '同理心得分(EMI)', " +
+                    "ADD COLUMN social_adaptability_score INT COMMENT '社会适应得分(EMI)', " +
+                    "ADD COLUMN self_motivation_score INT COMMENT '自我激励得分(EMI)'");
+                log.info("已为 dan_assessment_results 表添加 EMI 维度字段");
+            } else {
+                log.info("dan_assessment_results EMI 维度字段已存在,跳过");
+            }
+        } catch (Exception e) {
+            log.warn("检查/添加 dan_assessment_results EMI 维度字段失败: {}", e.getMessage());
+        }
+
+        // 迁移: dan_assessment_results 表添加 EMI 心理子维度字段
+        try {
+            Integer colExists = jdbcTemplate.queryForObject(
+                "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'dan_assessment_results' AND COLUMN_NAME = 'emotion_score'",
+                Integer.class);
+            if (colExists == null || colExists == 0) {
+                jdbcTemplate.execute("ALTER TABLE dan_assessment_results " +
+                    "ADD COLUMN emotion_score INT COMMENT '情绪商数(0-100)', " +
+                    "ADD COLUMN resilience_score INT COMMENT '心理韧性(0-100)', " +
+                    "ADD COLUMN stress_coping_score INT COMMENT '压力应对(0-100)', " +
+                    "ADD COLUMN self_awareness_score INT COMMENT '自我认知(0-100)'");
+                log.info("已为 dan_assessment_results 表添加 EMI 心理子维度字段");
+            } else {
+                log.info("dan_assessment_results EMI 心理子维度字段已存在,跳过");
+            }
+        } catch (Exception e) {
+            log.warn("检查/添加 dan_assessment_results EMI 心理子维度字段失败: {}", e.getMessage());
+        }
+
+        // 迁移: users 表添加出生时辰字段
+        try {
+            jdbcTemplate.execute("ALTER TABLE users ADD COLUMN birth_hour VARCHAR(10) COMMENT '出生时辰: 子丑寅卯辰巳午未申酉戌亥'");
+            log.info("已添加birth_hour列到users表");
+        } catch (Exception e) {
+            // 列已存在,忽略错误
+        }
+
+        // 迁移: 重建 family_member_attributes 表结构(v1.2 心智模型)
+        try {
+            Integer colExists = jdbcTemplate.queryForObject(
+                "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'family_member_attributes' AND COLUMN_NAME = 'member_id'",
+                Integer.class);
+            if (colExists == null || colExists == 0) {
+                // 旧表存在但结构不同,先删除旧表重建
+                jdbcTemplate.execute("DROP TABLE IF EXISTS family_member_attributes_old");
+                jdbcTemplate.execute("RENAME TABLE family_member_attributes TO family_member_attributes_old");
+                jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS family_member_attributes (" +
+                    "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+                    "member_id BIGINT NOT NULL COMMENT '家庭成员ID(children表或users表)', " +
+                    "member_type VARCHAR(10) NOT NULL COMMENT '成员类型: child/parent', " +
+                    "birth_datetime DATETIME COMMENT '出生日期(精确到分钟,用于八字排盘)', " +
+                    "eight_characters JSON COMMENT '八字四柱JSON', " +
+                    "wuxing_elements JSON COMMENT '五行星盘元素JSON', " +
+                    "zodiac VARCHAR(16) COMMENT '生肖: rat/ox/tiger/rabbit/dragon/snake/horse/sheep/monkey/rooster/dog/pig', " +
+                    "blood_type VARCHAR(5) COMMENT '血型: A/B/AB/O', " +
+                    "mind_base_score DECIMAL(5,2) DEFAULT 0 COMMENT '心维度基础分(先天)', " +
+                    "wisdom_base_score DECIMAL(5,2) DEFAULT 0 COMMENT '智维度基础分(先天)', " +
+                    "behavior_modifier JSON COMMENT '行为修正因子JSON', " +
+                    "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+                    "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
+                    "UNIQUE KEY uk_member (member_id, member_type)" +
+                    ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='家庭成员先天属性表'");
+                log.info("已重建 family_member_attributes 表(v1.2 心智模型)");
+            } else {
+                log.info("family_member_attributes 表已为 v1.2 结构,跳过");
+            }
+        } catch (Exception e) {
+            log.warn("重建 family_member_attributes 表失败: {}", e.getMessage());
+        }
+
         log.info("数据库迁移完成");
     }
 
@@ -1697,6 +1779,80 @@ try {
   log.warn("初始化测试用户失败: {}", e.getMessage());
 }
 
+        // 初始化星座配置种子数据
+        try {
+            Integer count = jdbcTemplate.queryForObject(
+                "SELECT COUNT(*) FROM zodiac_config", Integer.class);
+            if (count == null || count == 0) {
+                jdbcTemplate.execute("INSERT INTO zodiac_config (zodiac_code, zodiac_name, element, start_month, start_day, end_month, end_day, mind_base, wisdom_base, body_modifier, action_modifier, wealth_modifier) VALUES " +
+                    "('aries',       '白羊座', '火', 3, 21, 4, 19, 65, 70, 1.10, 1.05, 0.95), " +
+                    "('taurus',      '金牛座', '土', 4, 20, 5, 20, 60, 65, 1.05, 1.10, 1.15), " +
+                    "('gemini',      '双子座', '金', 5, 21, 6, 21, 70, 80, 0.95, 1.15, 0.90), " +
+                    "('cancer',      '巨蟹座', '水', 6, 22, 7, 22, 75, 60, 1.00, 0.90, 1.05), " +
+                    "('leo',         '狮子座', '火', 7, 23, 8, 22, 65, 75, 1.15, 1.00, 1.10), " +
+                    "('virgo',       '处女座', '土', 8, 23, 9, 22, 55, 85, 0.95, 1.05, 1.00), " +
+                    "('libra',       '天秤座', '金', 9, 23, 10, 23, 70, 75, 0.90, 0.95, 1.10), " +
+                    "('scorpio',     '天蝎座', '水', 10, 24, 11, 22, 80, 70, 1.10, 1.00, 1.15), " +
+                    "('sagittarius', '射手座', '火', 11, 23, 12, 21, 60, 65, 1.15, 1.15, 0.95), " +
+                    "('capricorn',   '摩羯座', '土', 12, 22, 1, 19, 55, 75, 1.05, 1.10, 1.20), " +
+                    "('aquarius',    '水瓶座', '金', 1, 20, 2, 18, 75, 85, 0.95, 0.90, 1.00), " +
+                    "('pisces',      '双鱼座', '水', 2, 19, 3, 20, 80, 60, 0.90, 0.95, 0.95)");
+                log.info("星座配置种子数据已初始化");
+            }
+        } catch (Exception e) {
+            log.warn("初始化星座配置失败: {}", e.getMessage());
+        }
+
+        // 初始化八字配置种子数据(十天干 + 十二地支)
+        try {
+            Integer count = jdbcTemplate.queryForObject(
+                "SELECT COUNT(*) FROM bazi_config", Integer.class);
+            if (count == null || count == 0) {
+                jdbcTemplate.execute("INSERT INTO bazi_config (stem_or_branch, type, element, yin_yang, mind_base, wisdom_base, body_modifier, action_modifier, wealth_modifier) VALUES " +
+                    "('甲', 'stem', '木', 'yang', 60, 75, 1.05, 1.10, 1.00), " +
+                    "('乙', 'stem', '木', 'yin',  65, 70, 0.95, 1.05, 1.00), " +
+                    "('丙', 'stem', '火', 'yang', 75, 65, 1.10, 1.00, 0.95), " +
+                    "('丁', 'stem', '火', 'yin',  70, 60, 1.05, 0.95, 1.00), " +
+                    "('戊', 'stem', '土', 'yang', 60, 65, 1.15, 1.05, 1.10), " +
+                    "('己', 'stem', '土', 'yin',  55, 60, 1.10, 1.00, 1.15), " +
+                    "('庚', 'stem', '金', 'yang', 65, 80, 1.05, 1.10, 1.05), " +
+                    "('辛', 'stem', '金', 'yin',  60, 75, 0.95, 1.00, 1.10), " +
+                    "('壬', 'stem', '水', 'yang', 70, 70, 1.00, 0.95, 1.15), " +
+                    "('癸', 'stem', '水', 'yin',  75, 65, 0.95, 0.90, 1.10), " +
+                    "('子', 'branch', '水', 'yang', 70, 65, 1.00, 0.95, 1.05), " +
+                    "('丑', 'branch', '土', 'yin',  55, 60, 1.10, 1.05, 1.10), " +
+                    "('寅', 'branch', '木', 'yang', 65, 75, 1.15, 1.10, 1.00), " +
+                    "('卯', 'branch', '木', 'yin',  60, 70, 1.05, 1.05, 0.95), " +
+                    "('辰', 'branch', '土', 'yang', 60, 65, 1.10, 1.00, 1.10), " +
+                    "('巳', 'branch', '火', 'yin',  75, 70, 1.05, 0.95, 1.00), " +
+                    "('午', 'branch', '火', 'yang', 80, 65, 1.15, 1.00, 0.95), " +
+                    "('未', 'branch', '土', 'yin',  55, 60, 1.05, 1.00, 1.05), " +
+                    "('申', 'branch', '金', 'yang', 65, 80, 1.00, 1.10, 1.05), " +
+                    "('酉', 'branch', '金', 'yin',  60, 75, 0.95, 1.05, 1.10), " +
+                    "('戌', 'branch', '土', 'yang', 60, 65, 1.10, 1.00, 1.15), " +
+                    "('亥', 'branch', '水', 'yin',  70, 60, 1.00, 0.95, 1.05)");
+                log.info("八字配置种子数据已初始化");
+            }
+        } catch (Exception e) {
+            log.warn("初始化八字配置失败: {}", e.getMessage());
+        }
+
+        // 初始化血型配置种子数据
+        try {
+            Integer count = jdbcTemplate.queryForObject(
+                "SELECT COUNT(*) FROM blood_type_config", Integer.class);
+            if (count == null || count == 0) {
+                jdbcTemplate.execute("INSERT INTO blood_type_config (blood_type, description, mind_base, wisdom_base, body_modifier, action_modifier, wealth_modifier) VALUES " +
+                    "('A',  '认真负责、严谨细致,但易焦虑', 70, 75, 0.95, 1.00, 1.05), " +
+                    "('B',  '乐观开朗、好奇心强,但缺乏耐心', 65, 70, 1.05, 1.10, 0.95), " +
+                    "('AB', '理性冷静、适应力强,但优柔寡断', 75, 80, 1.00, 0.95, 1.10), " +
+                    "('O',  '自信果断、行动力强,但固执己见', 60, 65, 1.15, 1.15, 1.15)");
+                log.info("血型配置种子数据已初始化");
+            }
+        } catch (Exception e) {
+            log.warn("初始化血型配置失败: {}", e.getMessage());
+        }
+
         // 初始化成长规划师模块相关表
         initGuideModuleTables(jdbcTemplate);
     }

+ 3 - 2
cfc-backend/src/main/java/com/etotem/cfc/config/JwtInterceptor.java

@@ -17,14 +17,15 @@ public class JwtInterceptor implements HandlerInterceptor {
     @Resource
     private JwtConfig jwtConfig;
 
-    /** 无需登录的公开路径前缀(兜底,与 WebConfig.excludePathPatterns 保持同步) */
     private static final String[] PUBLIC_PATHS = {
         "/api/auth/",
         "/api/product/list",
         "/api/product/detail",
         "/api/product/type-list",
         "/api/mini-game/list",
-        "/api/admin-auth/",
+        "/api/admin-auth/send-code",
+        "/api/admin-auth/login",
+        "/api/admin-auth/login-by-password",
         "/api/articles/list",
         "/api/articles/detail",
         "/api/articles/categories",

+ 2 - 2
cfc-backend/src/main/java/com/etotem/cfc/controller/HealthReportController.java

@@ -45,7 +45,7 @@ public class HealthReportController {
 
         // 构建报告主体
         HealthReport report = new HealthReport();
-        report.setChildId(childId);
+        report.setUserId(childId);
         report.setReportType(reportType);
         report.setOverallScore(params.get("overallScore") != null
                 ? Integer.valueOf(params.get("overallScore").toString()) : null);
@@ -117,7 +117,7 @@ public class HealthReportController {
         if (childId == null) {
             return Result.error("childId不能为空");
         }
-        List<HealthReport> reports = healthReportService.getChildReports(childId);
+        List<HealthReport> reports = healthReportService.getUserReports(childId);
         return Result.success(reports);
     }
 

+ 39 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/BaziConfigController.java

@@ -0,0 +1,39 @@
+package com.etotem.cfc.controller.admin;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.BaziConfig;
+import com.etotem.cfc.service.BaziConfigService;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.util.List;
+
+@RestController
+@RequestMapping("/api/admin/bazi-config")
+public class BaziConfigController {
+
+    @Resource
+    private BaziConfigService baziConfigService;
+
+    @PostMapping("/list")
+    public Result<List<BaziConfig>> list() {
+        return Result.success(baziConfigService.getAll());
+    }
+
+    @PostMapping("/create")
+    public Result<BaziConfig> create(@RequestBody BaziConfig config) {
+        return Result.success(baziConfigService.create(config));
+    }
+
+    @PostMapping("/update")
+    public Result<BaziConfig> update(@RequestBody BaziConfig config) {
+        boolean success = baziConfigService.update(config);
+        return success ? Result.success(config) : Result.error("更新失败");
+    }
+
+    @PostMapping("/delete")
+    public Result<Void> delete(@RequestParam Long id) {
+        boolean success = baziConfigService.delete(id);
+        return success ? Result.success(null) : Result.error("删除失败");
+    }
+}

+ 39 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/BloodTypeConfigController.java

@@ -0,0 +1,39 @@
+package com.etotem.cfc.controller.admin;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.BloodTypeConfig;
+import com.etotem.cfc.service.BloodTypeConfigService;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.util.List;
+
+@RestController
+@RequestMapping("/api/admin/blood-type-config")
+public class BloodTypeConfigController {
+
+    @Resource
+    private BloodTypeConfigService bloodTypeConfigService;
+
+    @PostMapping("/list")
+    public Result<List<BloodTypeConfig>> list() {
+        return Result.success(bloodTypeConfigService.getAll());
+    }
+
+    @PostMapping("/create")
+    public Result<BloodTypeConfig> create(@RequestBody BloodTypeConfig config) {
+        return Result.success(bloodTypeConfigService.create(config));
+    }
+
+    @PostMapping("/update")
+    public Result<BloodTypeConfig> update(@RequestBody BloodTypeConfig config) {
+        boolean success = bloodTypeConfigService.update(config);
+        return success ? Result.success(config) : Result.error("更新失败");
+    }
+
+    @PostMapping("/delete")
+    public Result<Void> delete(@RequestParam Long id) {
+        boolean success = bloodTypeConfigService.delete(id);
+        return success ? Result.success(null) : Result.error("删除失败");
+    }
+}

+ 54 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/FamilyAttributeController.java

@@ -0,0 +1,54 @@
+package com.etotem.cfc.controller.admin;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.FamilyMemberAttributes;
+import com.etotem.cfc.service.FamilyMemberAttributeService;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.math.BigDecimal;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/api/family/attributes")
+public class FamilyAttributeController {
+
+    @Resource
+    private FamilyMemberAttributeService familyMemberAttributeService;
+
+    @PostMapping("/update")
+    public Result<FamilyMemberAttributes> update(@RequestBody FamilyMemberAttributes attribute) {
+        FamilyMemberAttributes saved = familyMemberAttributeService.saveOrUpdateAttribute(attribute);
+        return Result.success(saved);
+    }
+
+    @PostMapping("/get")
+    public Result<FamilyMemberAttributes> get(@RequestBody Map<String, Object> params) {
+        Long memberId = Long.valueOf(params.get("memberId").toString());
+        String memberType = (String) params.get("memberType");
+        FamilyMemberAttributes attr = familyMemberAttributeService.getByMember(memberId, memberType);
+        return Result.success(attr);
+    }
+
+    @PostMapping("/calculate")
+    public Result<FamilyMemberAttributes> calculate(@RequestBody Map<String, Object> params) {
+        Long memberId = Long.valueOf(params.get("memberId").toString());
+        String memberType = (String) params.get("memberType");
+
+        FamilyMemberAttributes attrs = familyMemberAttributeService.getByMember(memberId, memberType);
+        if (attrs == null) {
+            return Result.error("未找到该成员的属性记录");
+        }
+
+        // Auto-calculate zodiac, BaZi, life number
+        familyMemberAttributeService.autoCalculate(attrs);
+
+        // Calculate innate scores for mind and wisdom dimensions
+        BigDecimal mindScore = familyMemberAttributeService.calcInnateScore(memberId, memberType, "mind");
+        BigDecimal wisdomScore = familyMemberAttributeService.calcInnateScore(memberId, memberType, "wisdom");
+
+        // Reload to get updated values
+        attrs = familyMemberAttributeService.getByMember(memberId, memberType);
+        return Result.success(attrs);
+    }
+}

+ 39 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/ZodiacConfigController.java

@@ -0,0 +1,39 @@
+package com.etotem.cfc.controller.admin;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.ZodiacConfig;
+import com.etotem.cfc.service.ZodiacConfigService;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.util.List;
+
+@RestController
+@RequestMapping("/api/admin/zodiac-config")
+public class ZodiacConfigController {
+
+    @Resource
+    private ZodiacConfigService zodiacConfigService;
+
+    @PostMapping("/list")
+    public Result<List<ZodiacConfig>> list() {
+        return Result.success(zodiacConfigService.getAll());
+    }
+
+    @PostMapping("/create")
+    public Result<ZodiacConfig> create(@RequestBody ZodiacConfig config) {
+        return Result.success(zodiacConfigService.create(config));
+    }
+
+    @PostMapping("/update")
+    public Result<ZodiacConfig> update(@RequestBody ZodiacConfig config) {
+        boolean success = zodiacConfigService.update(config);
+        return success ? Result.success(config) : Result.error("更新失败");
+    }
+
+    @PostMapping("/delete")
+    public Result<Void> delete(@RequestParam Long id) {
+        boolean success = zodiacConfigService.delete(id);
+        return success ? Result.success(null) : Result.error("删除失败");
+    }
+}

+ 38 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/BaziConfig.java

@@ -0,0 +1,38 @@
+package com.etotem.cfc.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.util.Date;
+
+@Data
+@TableName("bazi_config")
+public class BaziConfig implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private String stemOrBranch;
+
+    private String type;
+
+    private String element;
+
+    private String yinYang;
+
+    private BigDecimal mindBase;
+
+    private BigDecimal wisdomBase;
+
+    private BigDecimal bodyModifier;
+
+    private BigDecimal actionModifier;
+
+    private BigDecimal wealthModifier;
+
+    private Date createdAt;
+}

+ 34 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/BloodTypeConfig.java

@@ -0,0 +1,34 @@
+package com.etotem.cfc.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.util.Date;
+
+@Data
+@TableName("blood_type_config")
+public class BloodTypeConfig implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private String bloodType;
+
+    private String description;
+
+    private BigDecimal mindBase;
+
+    private BigDecimal wisdomBase;
+
+    private BigDecimal bodyModifier;
+
+    private BigDecimal actionModifier;
+
+    private BigDecimal wealthModifier;
+
+    private Date createdAt;
+}

+ 12 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/DanAssessmentResult.java

@@ -37,8 +37,20 @@ public class DanAssessmentResult implements Serializable {
 
     private Integer processingSpeedScore;  // 加工速度得分
 
+    // === EMI 心理子维度(v1.2 新增) ===
+    private Integer emotionScore;  // 情绪商数(0-100)
+    private Integer resilienceScore;  // 心理韧性(0-100)
+    private Integer stressCopingScore;  // 压力应对(0-100)
+    private Integer selfAwarenessScore;  // 自我认知(0-100)
+
     private Integer overallScore;  // 综合得分
 
+    // === EMI 维度(情绪商数评估) ===
+    private Integer emotionManagementScore;  // 情绪管理得分
+    private Integer empathyScore;  // 同理心得分
+    private Integer socialAdaptabilityScore;  // 社会适应得分
+    private Integer selfMotivationScore;  // 自我激励得分
+
     private Date assessmentDate;
 
     private Integer durationMinutes;  // 测评时长

+ 52 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/FamilyMemberAttributes.java

@@ -0,0 +1,52 @@
+package com.etotem.cfc.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.util.Date;
+
+@Data
+@TableName("family_member_attributes")
+public class FamilyMemberAttributes implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /** 家庭成员ID(children表或users表) */
+    private Long memberId;
+
+    /** 成员类型: child/parent */
+    private String memberType;
+
+    /** 出生日期(精确到分钟,用于八字排盘) */
+    private Date birthDatetime;
+
+    /** 八字四柱JSON: {"year":"甲子","month":"丙寅","day":"戊辰","hour":"壬申"} */
+    private String eightCharacters;
+
+    /** 五行星盘元素JSON: {"wood":30,"fire":45,"earth":60,"metal":25,"water":40} */
+    private String wuxingElements;
+
+    /** 生肖: rat/ox/tiger/rabbit/dragon/snake/horse/sheep/monkey/rooster/dog/pig */
+    private String zodiac;
+
+    /** 血型: A/B/AB/O */
+    private String bloodType;
+
+    /** 心维度基础分(先天,0-100) */
+    private BigDecimal mindBaseScore;
+
+    /** 智维度基础分(先天,0-100) */
+    private BigDecimal wisdomBaseScore;
+
+    /** 行为修正因子JSON,记录各来源的修正值 */
+    private String behaviorModifier;
+
+    private Date createdAt;
+
+    private Date updatedAt;
+}

+ 2 - 2
cfc-backend/src/main/java/com/etotem/cfc/entity/HealthReport.java

@@ -19,8 +19,8 @@ public class HealthReport implements Serializable {
     @TableId(type = IdType.AUTO)
     private Long id;
 
-    /** 孩子ID */
-    private Long childId;
+    /** 用户ID(关联users表) */
+    private Long userId;
 
     /** 家庭ID */
     private Long familyId;

+ 3 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/User.java

@@ -41,6 +41,9 @@ public class User implements Serializable {
 
     private Date birthday; // 生日
 
+    /** 出生时辰: 子丑寅卯辰巳午未申酉戌亥 */
+    private String birthHour;
+
     private String gender; // 性别: male/female
 
     // 家长积分 (孩子没有积分,用积分奖励家长)

+ 44 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/ZodiacConfig.java

@@ -0,0 +1,44 @@
+package com.etotem.cfc.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.util.Date;
+
+@Data
+@TableName("zodiac_config")
+public class ZodiacConfig implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private String zodiacCode;
+
+    private String zodiacName;
+
+    private String element;
+
+    private Integer startMonth;
+
+    private Integer startDay;
+
+    private Integer endMonth;
+
+    private Integer endDay;
+
+    private BigDecimal mindBase;
+
+    private BigDecimal wisdomBase;
+
+    private BigDecimal bodyModifier;
+
+    private BigDecimal actionModifier;
+
+    private BigDecimal wealthModifier;
+
+    private Date createdAt;
+}

+ 9 - 0
cfc-backend/src/main/java/com/etotem/cfc/mapper/BaziConfigMapper.java

@@ -0,0 +1,9 @@
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.BaziConfig;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface BaziConfigMapper extends BaseMapper<BaziConfig> {
+}

+ 9 - 0
cfc-backend/src/main/java/com/etotem/cfc/mapper/BloodTypeConfigMapper.java

@@ -0,0 +1,9 @@
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.BloodTypeConfig;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface BloodTypeConfigMapper extends BaseMapper<BloodTypeConfig> {
+}

+ 9 - 0
cfc-backend/src/main/java/com/etotem/cfc/mapper/FamilyMemberAttributesMapper.java

@@ -0,0 +1,9 @@
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.FamilyMemberAttributes;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface FamilyMemberAttributesMapper extends BaseMapper<FamilyMemberAttributes> {
+}

+ 9 - 0
cfc-backend/src/main/java/com/etotem/cfc/mapper/ZodiacConfigMapper.java

@@ -0,0 +1,9 @@
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.ZodiacConfig;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface ZodiacConfigMapper extends BaseMapper<ZodiacConfig> {
+}

+ 29 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/BaziConfigService.java

@@ -0,0 +1,29 @@
+package com.etotem.cfc.service;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.etotem.cfc.entity.BaziConfig;
+import com.etotem.cfc.mapper.BaziConfigMapper;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+@Service
+public class BaziConfigService extends ServiceImpl<BaziConfigMapper, BaziConfig> {
+
+    public List<BaziConfig> getAll() {
+        return this.list();
+    }
+
+    public BaziConfig create(BaziConfig config) {
+        this.save(config);
+        return config;
+    }
+
+    public boolean update(BaziConfig config) {
+        return this.updateById(config);
+    }
+
+    public boolean delete(Long id) {
+        return this.removeById(id);
+    }
+}

+ 29 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/BloodTypeConfigService.java

@@ -0,0 +1,29 @@
+package com.etotem.cfc.service;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.etotem.cfc.entity.BloodTypeConfig;
+import com.etotem.cfc.mapper.BloodTypeConfigMapper;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+@Service
+public class BloodTypeConfigService extends ServiceImpl<BloodTypeConfigMapper, BloodTypeConfig> {
+
+    public List<BloodTypeConfig> getAll() {
+        return this.list();
+    }
+
+    public BloodTypeConfig create(BloodTypeConfig config) {
+        this.save(config);
+        return config;
+    }
+
+    public boolean update(BloodTypeConfig config) {
+        return this.updateById(config);
+    }
+
+    public boolean delete(Long id) {
+        return this.removeById(id);
+    }
+}

+ 1 - 1
cfc-backend/src/main/java/com/etotem/cfc/service/ContentSectionService.java

@@ -64,7 +64,7 @@ public class ContentSectionService {
                 // 未登录用户只能看到public区块
                 return roles.contains("anonymous");
             }
-            return roles.contains(role);
+            return roles.contains(role) || roles.contains("anonymous");
         } catch (JsonProcessingException e) {
             log.warn("解析allowedRoles失败: {}", allowedRolesJson, e);
             return false;

+ 148 - 11
cfc-backend/src/main/java/com/etotem/cfc/service/EnergyService.java

@@ -52,6 +52,9 @@ public class EnergyService {
     @Resource
     private HealthReportService healthReportService;
 
+    @Resource
+    private FamilyMemberAttributesMapper familyMemberAttributesMapper;
+
     /**
      * 计算整个家庭的能量沙盘数据
      *
@@ -159,7 +162,7 @@ public class EnergyService {
                             .eq(Child::getFamilyId, parent.getFamilyId())
             );
             for (Child child : children) {
-                Integer healthScore = healthReportService.calculateBodyScore(child.getId());
+                Integer healthScore = healthReportService.calculateBodyScore(child.getUserId());
                 if (healthScore != null) {
                     return healthScore;
                 }
@@ -229,7 +232,7 @@ public class EnergyService {
      */
     private int calcChildBody(Child child) {
         // 1. 优先使用健康报告中的身体评分
-        Integer healthScore = healthReportService.calculateBodyScore(child.getId());
+        Integer healthScore = healthReportService.calculateBodyScore(child.getUserId());
         if (healthScore != null) {
             return healthScore;
         }
@@ -238,14 +241,149 @@ public class EnergyService {
                 Arrays.asList("运动", "体育", "户外", "健康"), 365);
     }
 
-    /** 孩子 心 — 学习/阅读类任务完成率 */
+    /** 孩子 心 — DAN EMI 70% + 先天 30% + 行为修正 */
     private int calcChildMind(Child child) {
-        return calcTaskCompletionRate(child.getId(), "child",
+        Long childId = child.getId();
+        Long userId = child.getUserId();
+
+        // 1. DAN EMI score
+        DanAssessmentResult dan = findLatestDanResult(childId);
+        double danScore = -1;
+        if (dan != null) {
+            Integer ems = dan.getEmotionManagementScore();
+            Integer es = dan.getEmpathyScore();
+            Integer sas = dan.getSocialAdaptabilityScore();
+            Integer sms = dan.getSelfMotivationScore();
+            if (ems != null && es != null && sas != null && sms != null) {
+                danScore = (ems + es + sas + sms) / 4.0;
+            }
+        }
+
+        // 2. Innate score (先天)
+        double innateScore = calcInnateScore(userId, "mind");
+
+        // 3. Behavior modifier
+        double modifier = calcBehaviorModifier(childId, "mind");
+
+        // 4. Model: has DAN
+        if (danScore >= 0) {
+            double base = danScore * 0.70;
+            if (innateScore >= 0) {
+                base += innateScore * 0.30;
+            }
+            return clamp((int) Math.round(base * (1 + modifier)), 0, 100);
+        }
+
+        // 5. Fallback: innate only
+        if (innateScore >= 0) {
+            return clamp((int) Math.round(innateScore * (1 + modifier)), 0, 100);
+        }
+
+        // 6. Fallback: task completion rate (old logic)
+        return calcTaskCompletionRate(childId, "child",
                 Arrays.asList("学习", "阅读", "亲子"), 365);
     }
 
-    /** 孩子 智 — 游戏得分 + 测评结果 + 阅读时长 多源加权 */
+    /** 孩子 智 — DAN COG 70% + 先天 30% + 行为修正 */
     private int calcChildWisdom(Child child) {
+        Long childId = child.getId();
+        Long userId = child.getUserId();
+
+        // 1. DAN COG score - weighted average of 6 cognitive dimensions
+        DanAssessmentResult dan = findLatestDanResult(childId);
+        double danScore = -1;
+        if (dan != null) {
+            Integer as = dan.getAttentionScore();
+            Integer fs = dan.getFocusScore();
+            Integer ms = dan.getMemoryScore();
+            Integer ls = dan.getLogicScore();
+            Integer ps = dan.getPerceptionScore();
+            Integer ss = dan.getSpatialScore();
+            if (as != null && fs != null && ms != null && ls != null && ps != null && ss != null) {
+                danScore = as * 0.15 + fs * 0.15 + ms * 0.20 + ls * 0.20 + ps * 0.15 + ss * 0.15;
+            }
+        }
+
+        // 2. Innate score
+        double innateScore = calcInnateScore(userId, "wisdom");
+
+        // 3. Behavior modifier
+        double modifier = calcBehaviorModifier(childId, "wisdom");
+
+        // 4. Model: has DAN
+        if (danScore >= 0) {
+            double base = danScore * 0.70;
+            if (innateScore >= 0) {
+                base += innateScore * 0.30;
+            }
+            return clamp((int) Math.round(base * (1 + modifier)), 0, 100);
+        }
+
+        // 5. Fallback: innate only
+        if (innateScore >= 0) {
+            return clamp((int) Math.round(innateScore * (1 + modifier)), 0, 100);
+        }
+
+        // 6. Fallback: old multi-source weighted calculation
+        return calcOldChildWisdom(child);
+    }
+
+    /** Find the latest completed DAN assessment for a child */
+    private DanAssessmentResult findLatestDanResult(Long childId) {
+        LambdaQueryWrapper<DanAssessmentResult> qw = new LambdaQueryWrapper<DanAssessmentResult>()
+                .eq(DanAssessmentResult::getChildId, childId)
+                .eq(DanAssessmentResult::getStatus, "completed")
+                .orderByDesc(DanAssessmentResult::getAssessmentDate)
+                .last("LIMIT 1");
+        return danAssessmentResultMapper.selectOne(qw);
+    }
+
+    /** Calculate innate attribute base score for a dimension */
+    private double calcInnateScore(Long userId, String dimensionCode) {
+        FamilyMemberAttributes attrs = familyMemberAttributesMapper.selectOne(
+            new LambdaQueryWrapper<FamilyMemberAttributes>()
+                .eq(FamilyMemberAttributes::getMemberId, userId)
+                .eq(FamilyMemberAttributes::getMemberType, "parent")
+        );
+        if (attrs == null) return -1;
+
+        if ("mind".equals(dimensionCode) && attrs.getMindBaseScore() != null) {
+            return attrs.getMindBaseScore().doubleValue();
+        }
+        if ("wisdom".equals(dimensionCode) && attrs.getWisdomBaseScore() != null) {
+            return attrs.getWisdomBaseScore().doubleValue();
+        }
+        return -1;
+    }
+
+    /** Calculate behavior modifier based on recent activity */
+    private double calcBehaviorModifier(Long childId, String dimensionCode) {
+        double modifier = 0;
+
+        // Task completion rate (±5%)
+        int taskRate = calcTaskCompletionRate(childId, "child",
+                Arrays.asList("学习", "阅读", "亲子", "好习惯", "运动"), 90);
+        modifier += (taskRate - 50) / 100.0 * 0.05;
+
+        // Game performance (±5%)
+        LambdaQueryWrapper<GameRecord> gw = new LambdaQueryWrapper<GameRecord>()
+                .eq(GameRecord::getChildId, childId);
+        List<GameRecord> games = gameRecordMapper.selectList(gw);
+        if (!games.isEmpty()) {
+            double avgScore = games.stream()
+                    .filter(g -> g.getScore() != null)
+                    .mapToInt(GameRecord::getScore)
+                    .average()
+                    .orElse(50);
+            modifier += (avgScore - 50) / 100.0 * 0.05;
+        }
+
+        // Clamp to ±0.20
+        return Math.max(-0.20, Math.min(0.20, modifier));
+    }
+
+    /** Old wisdom calculation as fallback (游戏 + 测评overallScore + 阅读时长) */
+    private int calcOldChildWisdom(Child child) {
         Long childId = child.getId();
         double totalWeight = 0;
         double weightedSum = 0;
@@ -265,12 +403,7 @@ public class EnergyService {
         }
 
         // 2. 最近一次完成的测评得分
-        LambdaQueryWrapper<DanAssessmentResult> aw = new LambdaQueryWrapper<DanAssessmentResult>()
-                .eq(DanAssessmentResult::getChildId, childId)
-                .eq(DanAssessmentResult::getStatus, "completed")
-                .orderByDesc(DanAssessmentResult::getAssessmentDate)
-                .last("LIMIT 1");
-        DanAssessmentResult result = danAssessmentResultMapper.selectOne(aw);
+        DanAssessmentResult result = findLatestDanResult(childId);
         if (result != null && result.getOverallScore() != null) {
             weightedSum += result.getOverallScore().doubleValue() * 0.33;
             totalWeight += 0.33;
@@ -293,6 +426,10 @@ public class EnergyService {
         return Math.min(Math.max(score, 0), 100);
     }
 
+    private int clamp(int value, int min, int max) {
+        return Math.max(min, Math.min(max, value));
+    }
+
     /** 孩子 行 — 打卡天数 + 习惯类任务完成率 */
     private int calcChildAction(Child child) {
         Long childId = child.getId();

+ 431 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/FamilyMemberAttributeService.java

@@ -0,0 +1,431 @@
+package com.etotem.cfc.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.etotem.cfc.entity.*;
+import com.etotem.cfc.mapper.FamilyMemberAttributesMapper;
+import com.etotem.cfc.mapper.ZodiacConfigMapper;
+import com.etotem.cfc.mapper.BaziConfigMapper;
+import com.etotem.cfc.mapper.BloodTypeConfigMapper;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.*;
+import java.util.Calendar;
+
+@Service
+public class FamilyMemberAttributeService extends ServiceImpl<FamilyMemberAttributesMapper, FamilyMemberAttributes> {
+
+    @Resource
+    private ZodiacConfigMapper zodiacConfigMapper;
+
+    @Resource
+    private BaziConfigMapper baziConfigMapper;
+
+    @Resource
+    private BloodTypeConfigMapper bloodTypeConfigMapper;
+
+    // ========== Chinese Zodiac constants ==========
+    private static final String[] ZODIAC_CODES = {"rat", "ox", "tiger", "rabbit", "dragon", "snake",
+            "horse", "sheep", "monkey", "rooster", "dog", "pig"};
+
+    // ========== Heavenly Stems & Earthly Branches ==========
+    private static final String[] HEAVENLY_STEMS = {"甲", "乙", "丙", "丁", "戊", "己", "庚", "辛", "壬", "癸"};
+    private static final String[] EARTHLY_BRANCHES = {"子", "丑", "寅", "卯", "辰", "巳", "午", "未", "申", "酉", "戌", "亥"};
+
+    // ========== Month pillar lookup: month earthly branches (1=寅, 2=卯, ..., 12=丑) ==========
+    private static final int[] MONTH_BRANCH_INDEX = {2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 0, 1}; // month 1-12 -> branch index
+
+    // ========== Hour pillar: earthly branch by two-hour period ==========
+    private static final int[] HOUR_BRANCH_MAP = new int[24];
+    static {
+        // 23-0: 子(0), 1-2: 丑(1), 3-4: 寅(2), ... 21-22: 亥(11)
+        HOUR_BRANCH_MAP[23] = 0; HOUR_BRANCH_MAP[0] = 0;
+        HOUR_BRANCH_MAP[1] = 1;  HOUR_BRANCH_MAP[2] = 1;
+        HOUR_BRANCH_MAP[3] = 2;  HOUR_BRANCH_MAP[4] = 2;
+        HOUR_BRANCH_MAP[5] = 3;  HOUR_BRANCH_MAP[6] = 3;
+        HOUR_BRANCH_MAP[7] = 4;  HOUR_BRANCH_MAP[8] = 4;
+        HOUR_BRANCH_MAP[9] = 5;  HOUR_BRANCH_MAP[10] = 5;
+        HOUR_BRANCH_MAP[11] = 6; HOUR_BRANCH_MAP[12] = 6;
+        HOUR_BRANCH_MAP[13] = 7; HOUR_BRANCH_MAP[14] = 7;
+        HOUR_BRANCH_MAP[15] = 8; HOUR_BRANCH_MAP[16] = 8;
+        HOUR_BRANCH_MAP[17] = 9; HOUR_BRANCH_MAP[18] = 9;
+        HOUR_BRANCH_MAP[19] = 10; HOUR_BRANCH_MAP[20] = 10;
+        HOUR_BRANCH_MAP[21] = 11; HOUR_BRANCH_MAP[22] = 11;
+    }
+
+    /**
+     * Get attributes by memberId + memberType
+     */
+    public FamilyMemberAttributes getByMember(Long memberId, String memberType) {
+        LambdaQueryWrapper<FamilyMemberAttributes> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(FamilyMemberAttributes::getMemberId, memberId);
+        wrapper.eq(FamilyMemberAttributes::getMemberType, memberType);
+        return this.getOne(wrapper);
+    }
+
+    /**
+     * Save or update attributes (upsert by memberId + memberType)
+     */
+    public FamilyMemberAttributes saveOrUpdateAttribute(FamilyMemberAttributes attribute) {
+        if (attribute.getId() != null) {
+            attribute.setUpdatedAt(new Date());
+            this.updateById(attribute);
+            return attribute;
+        }
+        if (attribute.getMemberId() != null && attribute.getMemberType() != null) {
+            FamilyMemberAttributes existing = getByMember(attribute.getMemberId(), attribute.getMemberType());
+            if (existing != null) {
+                attribute.setId(existing.getId());
+                attribute.setCreatedAt(existing.getCreatedAt());
+                attribute.setUpdatedAt(new Date());
+                this.updateById(attribute);
+                return attribute;
+            }
+        }
+        attribute.setCreatedAt(new Date());
+        attribute.setUpdatedAt(new Date());
+        this.save(attribute);
+        return attribute;
+    }
+
+    /**
+     * Auto-calculate zodiac, eightCharacters (BaZi), and life number from birthDatetime.
+     * Also computes wuxingElements from the four pillars.
+     */
+    public void autoCalculate(FamilyMemberAttributes attrs) {
+        if (attrs.getBirthDatetime() == null) {
+            return;
+        }
+
+        Calendar cal = Calendar.getInstance();
+        cal.setTime(attrs.getBirthDatetime());
+        int year = cal.get(Calendar.YEAR);
+        int month = cal.get(Calendar.MONTH) + 1; // 1-based
+        int day = cal.get(Calendar.DAY_OF_MONTH);
+        int hour = cal.get(Calendar.HOUR_OF_DAY);
+
+        // --- Zodiac ---
+        String zodiac = ZODIAC_CODES[(year - 4) % 12];
+        attrs.setZodiac(zodiac);
+
+        // --- Year Pillar ---
+        int yearStemIdx = (year - 4) % 10;
+        int yearBranchIdx = (year - 4) % 12;
+        String yearPillar = HEAVENLY_STEMS[yearStemIdx] + EARTHLY_BRANCHES[yearBranchIdx];
+
+        // --- Month Pillar ---
+        // Month stem: based on year stem, using "五虎遁" rule
+        // 甲己之年丙作首 (stem 0,5 -> start 丙=2), 乙庚之年戊为头 (1,6 -> 戊=4),
+        // 丙辛之岁寻庚上 (2,7 -> 庚=6), 丁壬壬寅顺水流 (3,8 -> 壬=8),
+        // 戊癸甲寅好追求 (4,9 -> 甲=0)
+        int monthStemStart = new int[]{2, 4, 6, 8, 0}[(yearStemIdx % 5)];
+        int monthStemIdx = (monthStemStart + month - 1) % 10;
+        int monthBranchIdx = MONTH_BRANCH_INDEX[month - 1];
+        String monthPillar = HEAVENLY_STEMS[monthStemIdx] + EARTHLY_BRANCHES[monthBranchIdx];
+
+        // --- Day Pillar ---
+        // Simplified day pillar calculation using a known reference:
+        // Reference: 2000-01-01 = 甲子日 (stem=0, branch=0)
+        // Days from reference to target
+        Calendar ref = Calendar.getInstance();
+        ref.set(2000, Calendar.JANUARY, 1, 0, 0, 0);
+        ref.set(Calendar.MILLISECOND, 0);
+        long diffMs = cal.getTimeInMillis() - ref.getTimeInMillis();
+        long diffDays = diffMs / (24 * 60 * 60 * 1000);
+        int dayStemIdx = (int) ((0 + diffDays) % 10);
+        if (dayStemIdx < 0) dayStemIdx += 10;
+        int dayBranchIdx = (int) ((0 + diffDays) % 12);
+        if (dayBranchIdx < 0) dayBranchIdx += 12;
+        String dayPillar = HEAVENLY_STEMS[dayStemIdx] + EARTHLY_BRANCHES[dayBranchIdx];
+
+        // --- Hour Pillar ---
+        // Hour stem: based on day stem, using "五鼠遁" rule
+        // 甲己日起甲子 (0,5 -> start 甲=0), 乙庚日起丙子 (1,6 -> 丙=2),
+        // 丙辛日起戊子 (2,7 -> 戊=4), 丁壬日起庚子 (3,8 -> 庚=6),
+        // 戊癸日起壬子 (4,9 -> 壬=8)
+        int hourBranchIdx = HOUR_BRANCH_MAP[hour];
+        int hourStemStart = new int[]{0, 2, 4, 6, 8}[(dayStemIdx % 5)];
+        int hourStemIdx = (hourStemStart + hourBranchIdx) % 10;
+        String hourPillar = HEAVENLY_STEMS[hourStemIdx] + EARTHLY_BRANCHES[hourBranchIdx];
+
+        // --- Eight Characters JSON ---
+        Map<String, String> eightCharMap = new LinkedHashMap<>();
+        eightCharMap.put("year", yearPillar);
+        eightCharMap.put("month", monthPillar);
+        eightCharMap.put("day", dayPillar);
+        eightCharMap.put("hour", hourPillar);
+        attrs.setEightCharacters(toJson(eightCharMap));
+
+        // --- Wuxing Elements ---
+        // Count element occurrences from all 8 characters (4 stems + 4 branches)
+        Map<String, Integer> wuxingCount = new LinkedHashMap<>();
+        wuxingCount.put("wood", 0);
+        wuxingCount.put("fire", 0);
+        wuxingCount.put("earth", 0);
+        wuxingCount.put("metal", 0);
+        wuxingCount.put("water", 0);
+
+        String[] allStems = {HEAVENLY_STEMS[yearStemIdx], HEAVENLY_STEMS[monthStemIdx],
+                HEAVENLY_STEMS[dayStemIdx], HEAVENLY_STEMS[hourStemIdx]};
+        int[] allBranchIndices = {yearBranchIdx, monthBranchIdx, dayBranchIdx, hourBranchIdx};
+
+        for (String stem : allStems) {
+            String element = stemToElement(stem);
+            wuxingCount.put(element, wuxingCount.get(element) + 1);
+        }
+        for (int bIdx : allBranchIndices) {
+            String element = branchToElement(bIdx);
+            wuxingCount.put(element, wuxingCount.get(element) + 1);
+        }
+
+        // Convert counts to percentages (total = 8)
+        Map<String, Integer> wuxingPct = new LinkedHashMap<>();
+        for (Map.Entry<String, Integer> entry : wuxingCount.entrySet()) {
+            wuxingPct.put(entry.getKey(), (int) Math.round(entry.getValue() * 100.0 / 8));
+        }
+        attrs.setWuxingElements(toJson(wuxingPct));
+
+        // --- Life Number ---
+        int lifeNumber = calcLifeNumber(year, month, day);
+        // Store in behaviorModifier as a field (no dedicated column)
+        Map<String, Object> modifierMap = new LinkedHashMap<>();
+        if (attrs.getBehaviorModifier() != null && !attrs.getBehaviorModifier().isEmpty()) {
+            modifierMap = fromJson(attrs.getBehaviorModifier());
+        }
+        modifierMap.put("lifeNumber", lifeNumber);
+        attrs.setBehaviorModifier(toJson(modifierMap));
+    }
+
+    /**
+     * Calculate innate base score from zodiac_config + bazi_config + blood_type_config.
+     * Formula: zodiac × 50% + bazi (average of stems) × 30% + blood_type × 20%
+     *
+     * @param memberId    member ID
+     * @param memberType  child/parent
+     * @param dimensionCode "mind" or "wisdom"
+     * @return computed base score (0-100)
+     */
+    public BigDecimal calcInnateScore(Long memberId, String memberType, String dimensionCode) {
+        FamilyMemberAttributes attrs = getByMember(memberId, memberType);
+        if (attrs == null) {
+            return BigDecimal.ZERO;
+        }
+
+        // If already computed, return cached value
+        if ("mind".equals(dimensionCode) && attrs.getMindBaseScore() != null
+                && attrs.getMindBaseScore().compareTo(BigDecimal.ZERO) > 0) {
+            return attrs.getMindBaseScore();
+        }
+        if ("wisdom".equals(dimensionCode) && attrs.getWisdomBaseScore() != null
+                && attrs.getWisdomBaseScore().compareTo(BigDecimal.ZERO) > 0) {
+            return attrs.getWisdomBaseScore();
+        }
+
+        // --- Zodiac contribution (50%) ---
+        BigDecimal zodiacScore = BigDecimal.ZERO;
+        if (attrs.getZodiac() != null) {
+            LambdaQueryWrapper<ZodiacConfig> zodiacWrapper = new LambdaQueryWrapper<>();
+            zodiacWrapper.eq(ZodiacConfig::getZodiacCode, attrs.getZodiac());
+            ZodiacConfig zodiacConfig = zodiacConfigMapper.selectOne(zodiacWrapper);
+            if (zodiacConfig != null) {
+                zodiacScore = "mind".equals(dimensionCode) ? zodiacConfig.getMindBase() : zodiacConfig.getWisdomBase();
+                if (zodiacScore == null) zodiacScore = BigDecimal.ZERO;
+            }
+        }
+
+        // --- BaZi contribution (30%) - average of all stems/branches in eightCharacters ---
+        BigDecimal baziScore = BigDecimal.ZERO;
+        if (attrs.getEightCharacters() != null) {
+            Map<String, Object> eightCharMap = fromJson(attrs.getEightCharacters());
+            List<BigDecimal> scores = new ArrayList<>();
+            for (Map.Entry<String, Object> entry : eightCharMap.entrySet()) {
+                String pillar = String.valueOf(entry.getValue());
+                if (pillar != null && pillar.length() >= 1) {
+                    String stem = pillar.substring(0, 1);
+                    LambdaQueryWrapper<BaziConfig> baziWrapper = new LambdaQueryWrapper<>();
+                    baziWrapper.eq(BaziConfig::getStemOrBranch, stem);
+                    baziWrapper.eq(BaziConfig::getType, "stem");
+                    BaziConfig baziConfig = baziConfigMapper.selectOne(baziWrapper);
+                    if (baziConfig != null) {
+                        BigDecimal val = "mind".equals(dimensionCode) ? baziConfig.getMindBase() : baziConfig.getWisdomBase();
+                        if (val != null) scores.add(val);
+                    }
+                }
+                if (pillar != null && pillar.length() >= 2) {
+                    String branch = pillar.substring(1, 2);
+                    LambdaQueryWrapper<BaziConfig> baziWrapper = new LambdaQueryWrapper<>();
+                    baziWrapper.eq(BaziConfig::getStemOrBranch, branch);
+                    baziWrapper.eq(BaziConfig::getType, "branch");
+                    BaziConfig baziConfig = baziConfigMapper.selectOne(baziWrapper);
+                    if (baziConfig != null) {
+                        BigDecimal val = "mind".equals(dimensionCode) ? baziConfig.getMindBase() : baziConfig.getWisdomBase();
+                        if (val != null) scores.add(val);
+                    }
+                }
+            }
+            if (!scores.isEmpty()) {
+                BigDecimal sum = BigDecimal.ZERO;
+                for (BigDecimal s : scores) sum = sum.add(s);
+                baziScore = sum.divide(BigDecimal.valueOf(scores.size()), 4, RoundingMode.HALF_UP);
+            }
+        }
+
+        // --- Blood type contribution (20%) ---
+        BigDecimal bloodScore = BigDecimal.ZERO;
+        if (attrs.getBloodType() != null) {
+            LambdaQueryWrapper<BloodTypeConfig> bloodWrapper = new LambdaQueryWrapper<>();
+            bloodWrapper.eq(BloodTypeConfig::getBloodType, attrs.getBloodType());
+            BloodTypeConfig bloodConfig = bloodTypeConfigMapper.selectOne(bloodWrapper);
+            if (bloodConfig != null) {
+                bloodScore = "mind".equals(dimensionCode) ? bloodConfig.getMindBase() : bloodConfig.getWisdomBase();
+                if (bloodScore == null) bloodScore = BigDecimal.ZERO;
+            }
+        }
+
+        // --- Weighted sum ---
+        BigDecimal result = zodiacScore.multiply(BigDecimal.valueOf(0.5))
+                .add(baziScore.multiply(BigDecimal.valueOf(0.3)))
+                .add(bloodScore.multiply(BigDecimal.valueOf(0.2)))
+                .setScale(2, RoundingMode.HALF_UP);
+
+        // Cache the result
+        if ("mind".equals(dimensionCode)) {
+            attrs.setMindBaseScore(result);
+        } else {
+            attrs.setWisdomBaseScore(result);
+        }
+        this.updateById(attrs);
+
+        return result;
+    }
+
+    // ========== Helper methods ==========
+
+    /**
+     * Calculate life number: sum digits of YYYYMMDD until single digit
+     */
+    private int calcLifeNumber(int year, int month, int day) {
+        String dateStr = String.format("%04d%02d%02d", year, month, day);
+        int sum = 0;
+        for (char c : dateStr.toCharArray()) {
+            sum += (c - '0');
+        }
+        while (sum >= 10) {
+            int newSum = 0;
+            int n = sum;
+            while (n > 0) {
+                newSum += n % 10;
+                n /= 10;
+            }
+            sum = newSum;
+        }
+        return sum;
+    }
+
+    /**
+     * Map heavenly stem to wuxing element
+     */
+    private String stemToElement(String stem) {
+        switch (stem) {
+            case "甲": case "乙": return "wood";
+            case "丙": case "丁": return "fire";
+            case "戊": case "己": return "earth";
+            case "庚": case "辛": return "metal";
+            case "壬": case "癸": return "water";
+            default: return "earth";
+        }
+    }
+
+    /**
+     * Map earthly branch index to wuxing element
+     */
+    private String branchToElement(int branchIdx) {
+        // 子=水, 丑=土, 寅=木, 卯=木, 辰=土, 巳=火, 午=火, 未=土, 申=金, 酉=金, 戌=土, 亥=水
+        switch (branchIdx) {
+            case 0: case 11: return "water";  // 子, 亥
+            case 1: case 4: case 7: case 10: return "earth"; // 丑, 辰, 未, 戌
+            case 2: case 3: return "wood";    // 寅, 卯
+            case 5: case 6: return "fire";    // 巳, 午
+            case 8: case 9: return "metal";   // 申, 酉
+            default: return "earth";
+        }
+    }
+
+    /**
+     * Simple JSON serializer for Map
+     */
+    private String toJson(Map<String, ?> map) {
+        StringBuilder sb = new StringBuilder("{");
+        boolean first = true;
+        for (Map.Entry<String, ?> entry : map.entrySet()) {
+            if (!first) sb.append(",");
+            first = false;
+            sb.append("\"").append(entry.getKey()).append("\":");
+            Object val = entry.getValue();
+            if (val instanceof Number) {
+                sb.append(val);
+            } else {
+                sb.append("\"").append(val).append("\"");
+            }
+        }
+        sb.append("}");
+        return sb.toString();
+    }
+
+    /**
+     * Simple JSON parser for Map<String, Object>
+     */
+    @SuppressWarnings("unchecked")
+    private Map<String, Object> fromJson(String json) {
+        Map<String, Object> map = new LinkedHashMap<>();
+        if (json == null || json.isEmpty() || !json.startsWith("{")) {
+            return map;
+        }
+        String content = json.substring(1, json.length() - 1).trim();
+        if (content.isEmpty()) return map;
+        String[] pairs = content.split(",");
+        for (String pair : pairs) {
+            String[] kv = pair.split(":", 2);
+            if (kv.length == 2) {
+                String key = kv[0].trim().replace("\"", "");
+                String value = kv[1].trim().replace("\"", "");
+                // Try to parse as number
+                try {
+                    if (value.contains(".")) {
+                        map.put(key, Double.parseDouble(value));
+                    } else {
+                        map.put(key, Integer.parseInt(value));
+                    }
+                } catch (NumberFormatException e) {
+                    map.put(key, value);
+                }
+            }
+        }
+        return map;
+    }
+
+    /**
+     * Simple JSON parser for Map<String, String>
+     */
+    private Map<String, String> fromJson(String json, @SuppressWarnings("unused") boolean stringValues) {
+        Map<String, String> map = new LinkedHashMap<>();
+        if (json == null || json.isEmpty() || !json.startsWith("{")) {
+            return map;
+        }
+        String content = json.substring(1, json.length() - 1).trim();
+        if (content.isEmpty()) return map;
+        String[] pairs = content.split(",");
+        for (String pair : pairs) {
+            String[] kv = pair.split(":", 2);
+            if (kv.length == 2) {
+                String key = kv[0].trim().replace("\"", "");
+                String value = kv[1].trim().replace("\"", "");
+                map.put(key, value);
+            }
+        }
+        return map;
+    }
+}

+ 15 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/FinanceCheckinService.java

@@ -20,6 +20,9 @@ public class FinanceCheckinService {
     @Resource
     private FinanceCheckinMapper financeCheckinMapper;
 
+    @Resource
+    private EnergyService energyService;
+
     public List<FinanceCheckin> getCheckins(Long userId, Long childId, String yearMonth) {
         LambdaQueryWrapper<FinanceCheckin> wrapper = new LambdaQueryWrapper<FinanceCheckin>()
                 .eq(FinanceCheckin::getUserId, userId);
@@ -38,6 +41,18 @@ public class FinanceCheckinService {
     public FinanceCheckin createCheckin(FinanceCheckin checkin) {
         checkin.setCreatedAt(new Date());
         financeCheckinMapper.insert(checkin);
+
+        // Award energy points for completing a checkin
+        try {
+            Long childId = checkin.getChildId();
+            if (childId != null) {
+                energyService.awardEnergy(childId, "checkin", checkin.getId(), 5,
+                        "完成理财打卡", null);
+            }
+        } catch (Exception e) {
+            log.warn("理财打卡能量发放失败: checkinId={}, error={}", checkin.getId(), e.getMessage());
+        }
+
         return checkin;
     }
 

+ 9 - 9
cfc-backend/src/main/java/com/etotem/cfc/service/HealthReportService.java

@@ -49,17 +49,17 @@ public class HealthReportService {
         }
 
         log.info("健康报告已创建: id={}, childId={}, type={}, score={}",
-                report.getId(), report.getChildId(), report.getReportType(), report.getOverallScore());
+                report.getId(), report.getUserId(), report.getReportType(), report.getOverallScore());
         return report;
     }
 
     /**
      * 获取孩子最新的健康报告
      */
-    public HealthReport getLatestReport(Long childId) {
+    public HealthReport getLatestReport(Long userId) {
         return healthReportMapper.selectOne(
                 new LambdaQueryWrapper<HealthReport>()
-                        .eq(HealthReport::getChildId, childId)
+                        .eq(HealthReport::getUserId, userId)
                         .eq(HealthReport::getStatus, "active")
                         .orderByDesc(HealthReport::getReportDate)
                         .last("LIMIT 1")
@@ -67,12 +67,12 @@ public class HealthReportService {
     }
 
     /**
-     * 获取孩子的所有健康报告
+     * 获取用户的所有健康报告
      */
-    public List<HealthReport> getChildReports(Long childId) {
+    public List<HealthReport> getUserReports(Long userId) {
         return healthReportMapper.selectList(
                 new LambdaQueryWrapper<HealthReport>()
-                        .eq(HealthReport::getChildId, childId)
+                        .eq(HealthReport::getUserId, userId)
                         .eq(HealthReport::getStatus, "active")
                         .orderByDesc(HealthReport::getReportDate)
         );
@@ -103,7 +103,7 @@ public class HealthReportService {
     public Integer calculateBodyScore(Long childId) {
         List<HealthReport> reports = healthReportMapper.selectList(
                 new LambdaQueryWrapper<HealthReport>()
-                        .eq(HealthReport::getChildId, childId)
+                        .eq(HealthReport::getUserId, childId)
                         .eq(HealthReport::getStatus, "active")
                         .orderByDesc(HealthReport::getReportDate)
         );
@@ -151,8 +151,8 @@ public class HealthReportService {
     /**
      * 获取孩子身体维度的数据详情(健康指标解读)
      */
-    public List<HealthIndicator> getBodyDetail(Long childId) {
-        HealthReport latest = getLatestReport(childId);
+    public List<HealthIndicator> getBodyDetail(Long userId) {
+        HealthReport latest = getLatestReport(userId);
         if (latest == null) return Collections.emptyList();
         return getReportIndicators(latest.getId());
     }

+ 29 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/ZodiacConfigService.java

@@ -0,0 +1,29 @@
+package com.etotem.cfc.service;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.etotem.cfc.entity.ZodiacConfig;
+import com.etotem.cfc.mapper.ZodiacConfigMapper;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+@Service
+public class ZodiacConfigService extends ServiceImpl<ZodiacConfigMapper, ZodiacConfig> {
+
+    public List<ZodiacConfig> getAll() {
+        return this.list();
+    }
+
+    public ZodiacConfig create(ZodiacConfig config) {
+        this.save(config);
+        return config;
+    }
+
+    public boolean update(ZodiacConfig config) {
+        return this.updateById(config);
+    }
+
+    public boolean delete(Long id) {
+        return this.removeById(id);
+    }
+}

+ 75 - 1
cfc-backend/src/main/resources/schema.sql

@@ -26,6 +26,7 @@ CREATE TABLE IF NOT EXISTS users (
     phone VARCHAR(11),
     birthday DATE,
     gender ENUM('male','female'),
+    birth_hour VARCHAR(10) COMMENT '出生时辰: 子丑寅卯辰巳午未申酉戌亥',
     created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
     updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
     INDEX idx_openid (openid),
@@ -351,6 +352,13 @@ CREATE TABLE IF NOT EXISTS dan_assessment_results (
     focus_score INT COMMENT '专注力得分',
     memory_score INT COMMENT '记忆力得分',
     logic_score INT COMMENT '逻辑思维得分',
+    perception_score INT COMMENT '感知得分',
+    spatial_score INT COMMENT '空间思维得分',
+    processing_speed_score INT COMMENT '加工速度得分',
+    emotion_management_score INT COMMENT '情绪管理得分(EMI)',
+    empathy_score INT COMMENT '同理心得分(EMI)',
+    social_adaptability_score INT COMMENT '社会适应得分(EMI)',
+    self_motivation_score INT COMMENT '自我激励得分(EMI)',
     overall_score INT COMMENT '综合得分',
     assessment_date DATE NOT NULL,
     duration_minutes INT COMMENT '测评时长',
@@ -1367,7 +1375,7 @@ CREATE TABLE IF NOT EXISTS insurance_policies (
 -- 健康检测报告表
 CREATE TABLE IF NOT EXISTS health_reports (
     id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
-    child_id BIGINT NOT NULL COMMENT '孩子ID',
+    user_id BIGINT NOT NULL COMMENT '用户ID(关联users表)',
     family_id BIGINT COMMENT '家庭ID',
     report_type VARCHAR(32) NOT NULL COMMENT '报告类型: physical_exam(体检)/gut_flora(菌群检测)',
     report_date DATE COMMENT '报告日期',
@@ -1421,3 +1429,69 @@ INSERT INTO education_systems (name, label, description, sort_order) VALUES
 ('bachelor', '本科', '大学本科学历', 3),
 ('master', '硕士', '硕士研究生学历', 4),
 ('doctor', '博士', '博士研究生学历', 5);
+
+-- ===== v1.2 先天属性体系 =====
+
+-- 家庭成员先天属性表
+CREATE TABLE IF NOT EXISTS family_member_attributes (
+    id               BIGINT AUTO_INCREMENT PRIMARY KEY,
+    member_id        BIGINT       NOT NULL COMMENT '家庭成员ID(children表或users表)',
+    member_type      VARCHAR(10)  NOT NULL COMMENT '成员类型: child/parent',
+    birth_datetime   DATETIME     COMMENT '出生日期(精确到分钟,用于八字排盘)',
+    eight_characters JSON         COMMENT '八字四柱JSON',
+    wuxing_elements  JSON         COMMENT '五行星盘元素JSON',
+    zodiac           VARCHAR(16)  COMMENT '生肖: rat/ox/tiger/rabbit/dragon/snake/horse/sheep/monkey/rooster/dog/pig',
+    blood_type       VARCHAR(5)   COMMENT '血型: A/B/AB/O',
+    mind_base_score  DECIMAL(5,2) DEFAULT 0 COMMENT '心维度基础分(先天)',
+    wisdom_base_score DECIMAL(5,2) DEFAULT 0 COMMENT '智维度基础分(先天)',
+    behavior_modifier JSON        COMMENT '行为修正因子JSON',
+    created_at       DATETIME DEFAULT CURRENT_TIMESTAMP,
+    updated_at       DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    UNIQUE KEY uk_member (member_id, member_type)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='家庭成员先天属性表';
+
+-- 星座配置表
+CREATE TABLE IF NOT EXISTS zodiac_config (
+    id               BIGINT AUTO_INCREMENT PRIMARY KEY,
+    zodiac_code      VARCHAR(10)  NOT NULL UNIQUE COMMENT '星座代码',
+    zodiac_name      VARCHAR(10)  NOT NULL COMMENT '星座名称',
+    element          VARCHAR(10)  NOT NULL COMMENT '五行: 金木水火土',
+    start_month      INT          NOT NULL COMMENT '起始月',
+    start_day        INT          NOT NULL COMMENT '起始日',
+    end_month        INT          NOT NULL COMMENT '结束月',
+    end_day          INT          NOT NULL COMMENT '结束日',
+    mind_base        DECIMAL(4,2) DEFAULT 0 COMMENT '心先天基础分',
+    wisdom_base      DECIMAL(4,2) DEFAULT 0 COMMENT '智先天基础分',
+    body_modifier    DECIMAL(4,2) DEFAULT 0 COMMENT '身修正系数(%)',
+    action_modifier  DECIMAL(4,2) DEFAULT 0 COMMENT '行修正系数(%)',
+    wealth_modifier  DECIMAL(4,2) DEFAULT 0 COMMENT '富修正系数(%)',
+    created_at       DATETIME DEFAULT CURRENT_TIMESTAMP
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='星座配置表';
+
+-- 八字配置表
+CREATE TABLE IF NOT EXISTS bazi_config (
+    id               BIGINT AUTO_INCREMENT PRIMARY KEY,
+    stem_or_branch   VARCHAR(10)  NOT NULL COMMENT '天干或地支',
+    type             VARCHAR(5)   NOT NULL COMMENT '类型: stem/branch',
+    element          VARCHAR(10)  NOT NULL COMMENT '五行: 金木水火土',
+    yin_yang         VARCHAR(5)   COMMENT '阴阳: yin/yang',
+    mind_base        DECIMAL(4,2) DEFAULT 0 COMMENT '心先天基础分',
+    wisdom_base      DECIMAL(4,2) DEFAULT 0 COMMENT '智先天基础分',
+    body_modifier    DECIMAL(4,2) DEFAULT 0 COMMENT '身修正系数(%)',
+    action_modifier  DECIMAL(4,2) DEFAULT 0 COMMENT '行修正系数(%)',
+    wealth_modifier  DECIMAL(4,2) DEFAULT 0 COMMENT '富修正系数(%)',
+    created_at       DATETIME DEFAULT CURRENT_TIMESTAMP
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='八字配置表(天干地支五行)';
+
+-- 血型配置表
+CREATE TABLE IF NOT EXISTS blood_type_config (
+    id               BIGINT AUTO_INCREMENT PRIMARY KEY,
+    blood_type       VARCHAR(5)   NOT NULL UNIQUE COMMENT '血型: A/B/AB/O',
+    description      VARCHAR(200) COMMENT '性格描述',
+    mind_base        DECIMAL(4,2) DEFAULT 0 COMMENT '心先天基础分',
+    wisdom_base      DECIMAL(4,2) DEFAULT 0 COMMENT '智先天基础分',
+    body_modifier    DECIMAL(4,2) DEFAULT 0 COMMENT '身修正系数(%)',
+    action_modifier  DECIMAL(4,2) DEFAULT 0 COMMENT '行修正系数(%)',
+    wealth_modifier  DECIMAL(4,2) DEFAULT 0 COMMENT '富修正系数(%)',
+    created_at       DATETIME DEFAULT CURRENT_TIMESTAMP
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='血型配置表';

+ 4 - 0
cfc-frontend/pages.json

@@ -442,6 +442,10 @@
         {
           "path": "index",
           "style": { "navigationBarTitleText": "智慧" }
+        },
+        {
+          "path": "cognitive-report",
+          "style": { "navigationBarTitleText": "认知测评报告" }
         }
       ]
     },

+ 6 - 2
cfc-frontend/pages/action/index.vue

@@ -119,15 +119,19 @@ export default {
   },
   methods: {
     async loadSectionConfig() {
+      var baseSections = ['func_entries', 'hot_activities', 'recommended_products']
       if (!uni.getStorageSync('token')) {
-        this.visibleSections = ['func_entries', 'hot_activities', 'recommended_products']
+        this.visibleSections = baseSections
         return
       }
       var role = uni.getStorageSync('currentRole') || uni.getStorageSync('role') || null
       try {
         var res = await getVisibleSections({ pageKey: 'action', role: role })
         if (res.code === 200 && res.data) {
-          this.visibleSections = res.data.map(function(s) { return s.sectionKey })
+          var keys = res.data.map(function(s) { return s.sectionKey })
+          var merged = baseSections.slice()
+          keys.forEach(function(k) { if (merged.indexOf(k) === -1) merged.push(k) })
+          this.visibleSections = merged
         }
       } catch (e) {
         this.visibleSections = ['func_entries', 'hot_activities', 'recommended_products']

+ 6 - 2
cfc-frontend/pages/body/index.vue

@@ -156,15 +156,19 @@ export default {
   },
   methods: {
     async loadSectionConfig() {
+      var baseSections = ['func_entries']
       if (!uni.getStorageSync('token')) {
-        this.visibleSections = ['func_entries']
+        this.visibleSections = baseSections
         return
       }
       var role = uni.getStorageSync('currentRole') || uni.getStorageSync('role') || null
       try {
         var res = await getVisibleSections({ pageKey: 'body', role: role })
         if (res.code === 200 && res.data) {
-          this.visibleSections = res.data.map(function(s) { return s.sectionKey })
+          var keys = res.data.map(function(s) { return s.sectionKey })
+          var merged = baseSections.slice()
+          keys.forEach(function(k) { if (merged.indexOf(k) === -1) merged.push(k) })
+          this.visibleSections = merged
         }
       } catch (e) {
         this.visibleSections = ['func_entries']

+ 5 - 2
cfc-frontend/pages/index/parent-index.vue

@@ -1095,9 +1095,12 @@ b) 孩子卡片
    f) 待处理事项
    ======================================== */
 .pending-section { margin-bottom: 48rpx; }
-.pending-grid { display: flex; flex-wrap: wrap; gap: 16rpx; }
+.pending-grid {
+  display: grid;
+  grid-template-columns: repeat(2, 1fr);
+  gap: 16rpx;
+}
 .pending-card {
-  width: calc(50% - 8rpx);
   background: #fff;
   border-radius: 20rpx;
   padding: 24rpx;

+ 104 - 234
cfc-frontend/pages/login/login.vue

@@ -1,58 +1,42 @@
 <template>
   <view class="login-container">
+    <!-- Logo 区 -->
     <view class="logo-section">
       <image class="logo" src="/static/logo.png" mode="aspectFit" @error="handleImageError"></image>
       <text class="app-name">浠艾福</text>
-      <text class="slogan">家庭教育任务管理系统</text>
+      <text class="slogan">Care Family · 家庭健康成长俱乐部</text>
     </view>
 
-    <!-- 手机号登录 -->
-    <view class="login-form">
-      <view class="form-item">
-        <text class="label">手机号</text>
-        <input 
-          class="input" 
-          type="number" 
-          v-model="phone" 
-          placeholder="请输入手机号" 
-          maxlength="11"
-          cursor-spacing="100"
-        />
-      </view>
-      
-      <view class="form-item">
-        <text class="label">验证码</text>
-        <view class="code-input-wrapper">
-          <input 
-            class="input code-input" 
-            type="number" 
-            v-model="code" 
-            placeholder="请输入验证码" 
-            maxlength="6"
-          />
-          <button 
-            class="send-code-btn" 
-            :disabled="countdown > 0" 
-            @click="sendCode"
-          >
-            {{ countdown > 0 ? countdown + 's' : '发送验证码' }}
-          </button>
+    <!-- 平台简介 -->
+    <view class="intro-section">
+      <view class="intro-card">
+        <view class="intro-title">🌏 给全家的一站式幸福提案</view>
+        <view class="intro-desc">
+          围绕<span class="highlight">身、心、智、行、富</span>五个维度,用科技、智慧与温情,为您定制全生命周期解决方案。
         </view>
+        <view class="dimension-tags">
+          <view class="dim-tag dim-body">身 · 主动健康</view>
+          <view class="dim-tag dim-mind">心 · 情绪丰盈</view>
+          <view class="dim-tag dim-wisdom">智 · 赋能未来</view>
+          <view class="dim-tag dim-action">行 · 知行合一</view>
+          <view class="dim-tag dim-wealth">富 · 物质精神</view>
         </view>
+        <view class="intro-powered">亿图腾科技出品</view>
+      </view>
+    </view>
 
-        <button class="login-btn" :loading="loading" @click="phoneLogin">登录</button>
-
-        <!-- 微信一键登录 -->
-        <view class="wechat-section">
-          <button 
-            class="wechat-btn" 
-            open-type="getPhoneNumber" 
-            @getphonenumber="handleWechatPhoneLogin"
-          >
-            <text class="wechat-icon">微信</text>
-            <text>一键登录</text>
-          </button>
-        </view>
+    <!-- 微信一键登录 -->
+    <view class="login-form">
+      <view class="wechat-section">
+        <button
+          class="wechat-btn"
+          :loading="loading"
+          open-type="getPhoneNumber"
+          @getphonenumber="handleWechatPhoneLogin"
+        >
+          <text class="wechat-icon">微信</text>
+          <text>一键登录</text>
+        </button>
       </view>
 
       <view class="agreement">
@@ -67,18 +51,16 @@
         <text class="link" @click="showPrivacy">《隐私政策》</text>
       </view>
     </view>
+  </view>
 </template>
 
 <script>
-import { sendCode, phoneLogin as phoneLoginApi, wechatPhoneLogin, checkPhone, checkTeamInviteStatus, joinTeamByInvite } from '../../utils/api.js'
+import { wechatPhoneLogin, checkTeamInviteStatus, joinTeamByInvite } from '../../utils/api.js'
 
 export default {
   data() {
     return {
-      phone: '',
-      code: '',
       loading: false,
-      countdown: 0,
       agreed: false,
       nickname: '',
       avatar: '',
@@ -117,109 +99,13 @@ export default {
         showCancel: false
       })
     },
-    async sendCode() {
-      if (!this.phone) {
-        uni.showToast({ title: '请输入手机号', icon: 'none' })
-        return
-      }
-      if (!/^1[3-9]\d{9}$/.test(this.phone)) {
-        uni.showToast({ title: '手机号格式不正确', icon: 'none' })
-        return
-      }
-
-      try {
-        await sendCode({ phone: this.phone, type: 'login' })
-        uni.showToast({ title: '验证码已发送', icon: 'success' })
-        
-        // 开始倒计时
-        this.countdown = 60
-        const timer = setInterval(() => {
-          this.countdown--
-          if (this.countdown <= 0) {
-            clearInterval(timer)
-          }
-        }, 1000)
-      } catch (e) {
-        uni.showToast({ title: e.message || '发送失败', icon: 'none' })
-      }
-    },
-    async phoneLogin() {
-      if (!this.agreed) {
-        uni.showToast({ title: '请先同意用户协议', icon: 'none' })
-        return
-      }
-      if (!this.phone) {
-        uni.showToast({ title: '请输入手机号', icon: 'none' })
-        return
-      }
-      if (!this.code) {
-        uni.showToast({ title: '请输入验证码', icon: 'none' })
-        return
-      }
-
-      this.loading = true
-      try {
-        const res = await phoneLoginApi({
-          phone: this.phone,
-          code: this.code,
-          nickname: this.nickname || '用户',
-          avatar: this.avatar || ''
-        })
-
-        // 保存 token 和用户信息
-        uni.setStorageSync('token', res.data.token)
-        uni.setStorageSync('userId', res.data.userId)
-        uni.setStorageSync('role', res.data.role)
-        uni.setStorageSync('familyId', res.data.familyId)
-        // 登录后以本次登录角色为准,避免沿用上次切换视图
-        uni.setStorageSync('currentRole', res.data.role)
-        uni.setStorageSync('isSwitchedChild', false)
-        uni.setStorageSync('isSwitchedTeacher', false)
-        uni.setStorageSync('userInfo', {
-          nickname: res.data.nickname,
-          userId: res.data.userId
-        })
-
-        // 存储成长规划师审核状态
-        if (res.data.teacherStatus) {
-          uni.setStorageSync('teacherStatus', res.data.teacherStatus)
-        }
-        if (res.data.teacherRejectReason) {
-          uni.setStorageSync('teacherRejectReason', res.data.teacherRejectReason)
-        }
-
-        uni.showToast({ title: '登录成功', icon: 'success' })
-
-        // 根据角色跳转到对应首页,或进入信息编辑页面
-        setTimeout(() => {
-          if (res.data.needsRoleSelect) {
-            uni.redirectTo({
-              url: `/pages/user-edit/user-edit?token=${res.data.token}&userId=${res.data.userId}&needsRoleSelect=true`
-            })
-          } else {
-            this.handlePostLoginRouting()
-          }
-        }, 1000)
-      } catch (e) {
-        uni.showToast({ title: e.message || '登录失败', icon: 'none' })
-      } finally {
-        this.loading = false
-      }
-    },
-async handleWechatPhoneLogin(e) {
+    async handleWechatPhoneLogin(e) {
       if (e.detail.errMsg === 'getPhoneNumber:ok') {
         if (!this.agreed) {
           uni.showToast({ title: '请先同意用户协议', icon: 'none' })
           return
         }
 
-        // 测试环境:使用用户输入的手机号
-        const inputPhone = this.phone
-        if (!inputPhone || !/^1[3-9]\d{9}$/.test(inputPhone)) {
-          uni.showToast({ title: '请先输入手机号', icon: 'none' })
-          return
-        }
-
         this.loading = true
         try {
           // 获取登录 code 用于获取 openid
@@ -232,24 +118,22 @@ async handleWechatPhoneLogin(e) {
           })
 
           // 调用后端微信手机号登录接口
-          // 测试环境:将用户输入的手机号作为 phoneCode 传递
           const res = await wechatPhoneLogin({
-            code: loginRes.code, // 用于获取 openid
-            phoneCode: inputPhone, // 测试环境:用输入的手机号
+            code: loginRes.code,
             nickname: this.nickname || '用户',
             avatar: this.avatar || ''
           })
 
           // 保存 token 和用户信息
-          uni.setStorageSync('token', res.data.token)
-          uni.setStorageSync('userId', res.data.userId)
-          uni.setStorageSync('role', res.data.role)
-          uni.setStorageSync('familyId', res.data.familyId)
-          uni.setStorageSync('currentRole', res.data.role)
-          uni.setStorageSync('isSwitchedChild', false)
-          uni.setStorageSync('isSwitchedTeacher', false)
-          // 保存 openid,用于后续自动登录
-          if (res.data.openid) {
+          uni.setStorageSync('token', res.data.token)
+          uni.setStorageSync('userId', res.data.userId)
+          uni.setStorageSync('role', res.data.role)
+          uni.setStorageSync('familyId', res.data.familyId)
+          uni.setStorageSync('currentRole', res.data.role)
+          uni.setStorageSync('isSwitchedChild', false)
+          uni.setStorageSync('isSwitchedTeacher', false)
+          // 保存 openid,用于后续自动登录
+          if (res.data.openid) {
             uni.setStorageSync('openid', res.data.openid)
           }
           uni.setStorageSync('userInfo', {
@@ -282,7 +166,7 @@ async handleWechatPhoneLogin(e) {
         } finally {
           this.loading = false
         }
-    }
+      }
     },
     handlePostLoginRouting() {
       const inviteCode = uni.getStorageSync('inviteCode')
@@ -359,7 +243,7 @@ async handleWechatPhoneLogin(e) {
       }
     }
   }
-}
+}
 </script>
 
 <style scoped>
@@ -369,9 +253,10 @@ async handleWechatPhoneLogin(e) {
   padding: 60rpx 40rpx;
 }
 
+/* ===== Logo 区 ===== */
 .logo-section {
   text-align: center;
-  margin-bottom: 80rpx;
+  margin-bottom: 48rpx;
 }
 
 .logo {
@@ -383,7 +268,7 @@ async handleWechatPhoneLogin(e) {
 
 .app-name {
   display: block;
-  font-size: 48rpx;
+  font-size: 52rpx;
   font-weight: bold;
   color: #fff;
   margin-top: 20rpx;
@@ -396,95 +281,78 @@ async handleWechatPhoneLogin(e) {
   margin-top: 10rpx;
 }
 
-.login-form {
-  background: #fff;
-  border-radius: 20rpx;
-  padding: 40rpx;
+/* ===== 平台简介卡片 ===== */
+.intro-section {
+  margin-bottom: 48rpx;
 }
 
-.form-item {
-  margin-bottom: 30rpx;
-}
-
-.label {
-  display: block;
-  font-size: 28rpx;
-  color: #333;
-  margin-bottom: 10rpx;
-}
-
-.input {
-  width: 100%;
-  height: 80rpx;
-  background: #f5f5f5;
-  border-radius: 10rpx;
-  padding: 0 20rpx;
-  font-size: 28rpx;
+.intro-card {
+  background: rgba(255, 255, 255, 0.15);
+  border: 1rpx solid rgba(255, 255, 255, 0.3);
+  border-radius: 20rpx;
+  padding: 32rpx 28rpx;
+  backdrop-filter: blur(10px);
 }
 
-.code-input-wrapper {
-  display: flex;
-  align-items: center;
-  gap: 20rpx;
+.intro-title {
+  font-size: 30rpx;
+  font-weight: bold;
+  color: #fff;
+  text-align: center;
+  margin-bottom: 16rpx;
 }
 
-.code-input {
-  flex: 1;
+.intro-desc {
+  font-size: 24rpx;
+  color: rgba(255, 255, 255, 0.9);
+  line-height: 1.7;
+  text-align: center;
+  margin-bottom: 24rpx;
 }
 
-.send-code-btn {
-  width: 200rpx;
-  height: 80rpx;
-  background: #667eea;
-  color: #fff;
-  border-radius: 10rpx;
-  font-size: 24rpx;
-  line-height: 80rpx;
-  padding: 0;
+.intro-desc .highlight {
+  color: #FFD700;
+  font-weight: 600;
 }
 
-.send-code-btn[disabled] {
-  background: #ccc;
+.dimension-tags {
+  display: flex;
+  flex-wrap: wrap;
+  justify-content: center;
+  gap: 12rpx;
+  margin-bottom: 20rpx;
 }
 
-.login-btn {
-  width: 100%;
-  height: 88rpx;
-  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+.dim-tag {
+  font-size: 22rpx;
+  padding: 8rpx 16rpx;
+  border-radius: 30rpx;
+  background: rgba(255, 255, 255, 0.2);
   color: #fff;
-  border-radius: 44rpx;
-  font-size: 32rpx;
-  line-height: 88rpx;
-  margin-top: 20rpx;
+  font-weight: 500;
 }
 
-/* 角色选择器样式 */
-.role-select {
-  display: flex;
-  gap: 40rpx;
-  padding: 20rpx 0;
-}
+.dim-body { background: rgba(91, 155, 213, 0.4); }
+.dim-mind { background: rgba(236, 72, 153, 0.4); }
+.dim-wisdom { background: rgba(139, 92, 246, 0.4); }
+.dim-action { background: rgba(16, 185, 129, 0.4); }
+.dim-wealth { background: rgba(245, 158, 11, 0.4); }
 
-.role-option {
-  display: flex;
-  align-items: center;
-  gap: 10rpx;
+.intro-powered {
+  text-align: center;
+  font-size: 20rpx;
+  color: rgba(255, 255, 255, 0.6);
 }
 
-.picker-input {
-  width: 100%;
-  height: 80rpx;
-  background: #f5f5f5;
-  border-radius: 10rpx;
-  padding: 0 20rpx;
-  font-size: 28rpx;
-  line-height: 80rpx;
-  color: #333;
+/* ===== 登录表单 ===== */
+.login-form {
+  background: #fff;
+  border-radius: 20rpx;
+  padding: 40rpx;
 }
 
 .wechat-section {
-  margin-top: 40rpx;
-  text-align: center;
+  margin-bottom: 32rpx;
 }
 
 .wechat-btn {
@@ -505,18 +373,20 @@ async handleWechatPhoneLogin(e) {
 }
 
 .agreement {
-  margin-top: 40rpx;
+  margin-top: 16rpx;
   text-align: center;
   font-size: 24rpx;
-  color: #fff;
 }
 
 .agreement-text {
-  color: rgba(255, 255, 255, 0.8);
+  color: #666;
 }
 
 .link {
-  color: #ffd700;
+  color: #667eea;
+}
+
+.agreement-text.link {
   text-decoration: underline;
 }
 </style>

+ 14 - 2
cfc-frontend/pages/mind/index.vue

@@ -42,6 +42,14 @@
       <view class="placeholder-btn" @click="goLogin">立即登录</view>
     </view>
 
+    <!-- 已登录但无家庭成员数据 -->
+    <view class="radar-placeholder" v-else>
+      <view class="placeholder-icon">&#x1F9E0;</view>
+      <text class="placeholder-title">认知能力评估</text>
+      <text class="placeholder-desc">完成测评后查看六维认知雷达图,了解孩子的感知、专注、记忆等能力发展</text>
+      <view class="placeholder-btn" @click="goAssessment">预约测评</view>
+    </view>
+
     <!-- 功能入口 -->
     <view class="func-section" v-if="sectionVisible('func_entries')">
       <view class="func-grid">
@@ -208,15 +216,19 @@ export default {
   methods: {
     // ===== 区块可见性 =====
     async loadSectionConfig() {
+      var baseSections = ['func_entries', 'daily_tip', 'recommended_reading']
       if (!uni.getStorageSync('token')) {
-        this.visibleSections = ['func_entries', 'daily_tip', 'recommended_reading']
+        this.visibleSections = baseSections
         return
       }
       var role = uni.getStorageSync('currentRole') || uni.getStorageSync('role') || null
       try {
         var res = await getVisibleSections({ pageKey: 'mind', role: role })
         if (res.code === 200 && res.data) {
-          this.visibleSections = res.data.map(function(s) { return s.sectionKey })
+          var keys = res.data.map(function(s) { return s.sectionKey })
+          var merged = baseSections.slice()
+          keys.forEach(function(k) { if (merged.indexOf(k) === -1) merged.push(k) })
+          this.visibleSections = merged
         }
       } catch (e) {
         this.visibleSections = ['func_entries', 'daily_tip', 'recommended_reading', 'mind_tasks', 'related_activities', 'purchased_courses']

+ 17 - 0
cfc-frontend/pages/user-edit/user-edit.vue

@@ -96,6 +96,14 @@
 				</picker>
 			</view>
 
+			<!-- 出生时辰 -->
+			<view class="form-item">
+				<text class="label">出生时辰</text>
+				<picker mode="selector" :range="birthHourOptions" :value="birthHourIndex" @change="onBirthHourChange">
+					<view class="picker-input">{{ form.birthHour || '请选择出生时辰' }}</view>
+				</picker>
+			</view>
+
 			<!-- 民族 -->
 			<view class="form-item">
 				<text class="label">民族</text>
@@ -230,6 +238,7 @@ export default {
 				gender: '',
 				idCard: '',
 				birthday: '',
+				birthHour: '',
 				inviteCode: '',
 				phone: '',
 				teacherNo: '',
@@ -243,6 +252,8 @@ export default {
 			},
 			ethnicityOptions: ['汉族','蒙古族','回族','藏族','维吾尔族','苗族','彝族','壮族','布依族','朝鲜族','满族','侗族','瑶族','白族','土家族','哈尼族','哈萨克族','傣族','黎族','其他'],
 			bloodTypeOptions: ['A','B','AB','O','未知'],
+			birthHourOptions: ['子时(23-01)','丑时(01-03)','寅时(03-05)','卯时(05-07)','辰时(07-09)','巳时(09-11)','午时(11-13)','未时(13-15)','申时(15-17)','酉时(17-19)','戌时(19-21)','亥时(21-23)'],
+			birthHourValues: ['子','丑','寅','卯','辰','巳','午','未','申','酉','戌','亥'],
 			educationOptions: ['小学','初中','高中','中专','大专','本科','硕士','博士'],
 			maritalOptions: ['未婚','已婚','离异','丧偶'],
 			familyRoleOptions: ['爸爸','妈妈','爷爷','奶奶','姥爷','姥姥','其他'],
@@ -258,6 +269,9 @@ export default {
 		bloodTypeIndex() {
 			return this.bloodTypeOptions.indexOf(this.form.bloodType)
 		},
+		birthHourIndex() {
+			return this.birthHourValues.indexOf(this.form.birthHour)
+		},
 		educationIndex() {
 			return this.educationOptions.indexOf(this.form.highestEducation)
 		},
@@ -359,6 +373,9 @@ export default {
 		onBloodTypeChange(e) {
 			this.form.bloodType = this.bloodTypeOptions[e.detail.value]
 		},
+		onBirthHourChange(e) {
+			this.form.birthHour = this.birthHourValues[e.detail.value]
+		},
 		onEducationChange(e) {
 			this.form.highestEducation = this.educationOptions[e.detail.value]
 		},

+ 6 - 2
cfc-frontend/pages/wealth/index.vue

@@ -114,15 +114,19 @@ export default {
   },
   methods: {
     async loadSectionConfig() {
+      var baseSections = ['func_entries', 'hot_activities', 'recommended_products']
       if (!uni.getStorageSync('token')) {
-        this.visibleSections = ['func_entries', 'hot_activities', 'recommended_products']
+        this.visibleSections = baseSections
         return
       }
       var role = uni.getStorageSync('currentRole') || uni.getStorageSync('role') || null
       try {
         var res = await getVisibleSections({ pageKey: 'wealth', role: role })
         if (res.code === 200 && res.data) {
-          this.visibleSections = res.data.map(function(s) { return s.sectionKey })
+          var keys = res.data.map(function(s) { return s.sectionKey })
+          var merged = baseSections.slice()
+          keys.forEach(function(k) { if (merged.indexOf(k) === -1) merged.push(k) })
+          this.visibleSections = merged
         }
       } catch (e) {
         this.visibleSections = ['func_entries', 'hot_activities', 'recommended_products']

+ 131 - 0
cfc-frontend/pages/wisdom/cognitive-report.vue

@@ -0,0 +1,131 @@
+<template>
+  <view class="container">
+    <PageBanner theme="wisdom"
+      tagline="认知能力 · 全面评估"
+      quote="知人者智,自知者明" />
+
+    <view class="report-card" v-if="report">
+      <view class="report-header">
+        <text class="report-title">认知测评报告</text>
+        <text class="report-date">测评日期: {{ report.assessmentDate }}</text>
+      </view>
+
+      <view class="overall-score-wrap">
+        <text class="overall-label">综合认知评分</text>
+        <text class="overall-value">{{ report.overallScore || 0 }}</text>
+      </view>
+
+      <view class="dimension-list">
+        <view class="dimension-item" v-for="dim in dimensions" :key="dim.key">
+          <view class="dimension-header">
+            <text class="dimension-name">{{ dim.label }}</text>
+            <text class="dimension-score">{{ report[dim.key] || 0 }}</text>
+          </view>
+          <view class="dimension-bar">
+            <view class="dimension-bar-fill" :style="{width: (report[dim.key] || 0) + '%'}"></view>
+          </view>
+          <text class="dimension-desc">{{ dim.desc }}</text>
+        </view>
+      </view>
+
+      <view class="source-section">
+        <text class="source-title">数据源构成</text>
+        <view class="source-bar">
+          <view class="source-segment dan" style="width: 70%">COG 认知分 70%</view>
+          <view class="source-segment innate" style="width: 30%">先天基础 30%</view>
+        </view>
+      </view>
+
+      <view class="report-analysis" v-if="report.analysisReport">
+        <text class="analysis-title">分析报告</text>
+        <text class="analysis-content">{{ report.analysisReport }}</text>
+      </view>
+
+      <view class="report-suggestions" v-if="report.growthSuggestions">
+        <text class="suggestion-title">成长建议</text>
+        <text class="suggestion-content">{{ report.growthSuggestions }}</text>
+      </view>
+    </view>
+
+    <view class="empty-state" v-else>
+      <text class="empty-icon">&#x1F9E0;</text>
+      <text class="empty-title">暂无认知测评数据</text>
+      <text class="empty-desc">完成 DAN 认知评估后,可在此查看详细的六维认知分析报告</text>
+      <view class="empty-btn" @click="goAssessment">预约测评</view>
+    </view>
+  </view>
+</template>
+
+<script>
+import PageBanner from '../../components/PageBanner.vue'
+import { getEnergyOverview } from '../../utils/api.js'
+
+export default {
+  components: { PageBanner },
+  data() {
+    return {
+      report: null,
+      dimensions: [
+        { key: 'attentionScore', label: '注意力', desc: '信息筛选和专注能力' },
+        { key: 'focusScore', label: '专注力', desc: '持续集中和抗干扰能力' },
+        { key: 'memoryScore', label: '记忆力', desc: '信息编码和回忆能力' },
+        { key: 'logicScore', label: '逻辑思维', desc: '推理和问题解决能力' },
+        { key: 'perceptionScore', label: '感知能力', desc: '感官信息处理能力' },
+        { key: 'spatialScore', label: '空间思维', desc: '空间想象和方位判断' }
+      ]
+    }
+  },
+  onLoad() {
+    this.loadReport()
+  },
+  methods: {
+    async loadReport() {
+      try {
+        const childId = this.$store?.state?.currentChildId
+        if (!childId) return
+        const res = await getEnergyOverview(childId)
+        if (res.code === 200 && res.data) {
+          this.report = res.data
+        }
+      } catch (e) {
+        console.error('加载认知报告失败', e)
+      }
+    },
+    goAssessment() {
+      uni.navigateTo({ url: '/pages/assessment/index' })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.container { padding: 0 30rpx; }
+.report-card { background: #fff; border-radius: 24rpx; padding: 40rpx; margin-top: 30rpx; }
+.report-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 30rpx; }
+.report-title { font-size: 36rpx; font-weight: bold; color: #333; }
+.report-date { font-size: 24rpx; color: #999; }
+.overall-score-wrap { text-align: center; padding: 40rpx 0; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 20rpx; margin-bottom: 40rpx; color: #fff; }
+.overall-label { font-size: 28rpx; opacity: 0.9; }
+.overall-value { font-size: 80rpx; font-weight: bold; }
+.dimension-item { margin-bottom: 30rpx; }
+.dimension-header { display: flex; justify-content: space-between; margin-bottom: 10rpx; }
+.dimension-name { font-size: 28rpx; color: #333; }
+.dimension-score { font-size: 28rpx; color: #667eea; font-weight: bold; }
+.dimension-bar { height: 16rpx; background: #f0f0f0; border-radius: 8rpx; overflow: hidden; }
+.dimension-bar-fill { height: 100%; background: linear-gradient(90deg, #667eea, #764ba2); border-radius: 8rpx; transition: width 0.5s; }
+.dimension-desc { font-size: 22rpx; color: #999; margin-top: 6rpx; }
+.source-section { margin-top: 40rpx; padding: 30rpx; background: #f8f9ff; border-radius: 16rpx; }
+.source-title { font-size: 28rpx; font-weight: bold; color: #333; margin-bottom: 20rpx; display: block; }
+.source-bar { display: flex; height: 48rpx; border-radius: 24rpx; overflow: hidden; }
+.source-segment { display: flex; align-items: center; justify-content: center; font-size: 22rpx; color: #fff; }
+.source-segment.dan { background: #667eea; }
+.source-segment.innate { background: #f97316; }
+.report-analysis, .report-suggestions { margin-top: 30rpx; padding: 30rpx; background: #f9f9f9; border-radius: 16rpx; }
+.analysis-title, .suggestion-title { font-size: 30rpx; font-weight: bold; color: #333; margin-bottom: 16rpx; display: block; }
+.analysis-content, .suggestion-content { font-size: 26rpx; color: #666; line-height: 1.6; }
+.empty-state { text-align: center; padding: 100rpx 40rpx; }
+.empty-icon { font-size: 100rpx; }
+.empty-title { font-size: 32rpx; color: #333; margin-top: 30rpx; }
+.empty-desc { font-size: 26rpx; color: #999; margin-top: 16rpx; }
+.empty-btn { display: inline-block; margin-top: 40rpx; padding: 20rpx 60rpx; background: linear-gradient(135deg, #667eea, #764ba2); color: #fff; border-radius: 50rpx; font-size: 28rpx; }
+</style>

+ 6 - 2
cfc-frontend/pages/wisdom/index.vue

@@ -114,15 +114,19 @@ export default {
   },
   methods: {
     async loadSectionConfig() {
+      var baseSections = ['func_entries', 'hot_activities', 'recommended_products']
       if (!uni.getStorageSync('token')) {
-        this.visibleSections = ['func_entries', 'hot_activities', 'recommended_products']
+        this.visibleSections = baseSections
         return
       }
       var role = uni.getStorageSync('currentRole') || uni.getStorageSync('role') || null
       try {
         var res = await getVisibleSections({ pageKey: 'wisdom', role: role })
         if (res.code === 200 && res.data) {
-          this.visibleSections = res.data.map(function(s) { return s.sectionKey })
+          var keys = res.data.map(function(s) { return s.sectionKey })
+          var merged = baseSections.slice()
+          keys.forEach(function(k) { if (merged.indexOf(k) === -1) merged.push(k) })
+          this.visibleSections = merged
         }
       } catch (e) {
         this.visibleSections = ['func_entries', 'hot_activities', 'recommended_products']

+ 99 - 0
cfc-web/src/api/admin.js

@@ -546,3 +546,102 @@ export function deleteEducationSystem(id) {
     data: { id }
   })
 }
+
+// ========== 星座配置管理 API ==========
+
+export function listZodiacConfigs() {
+  return request({
+    url: '/api/admin/zodiac-config/list',
+    method: 'post'
+  })
+}
+
+export function createZodiacConfig(data) {
+  return request({
+    url: '/api/admin/zodiac-config/create',
+    method: 'post',
+    data
+  })
+}
+
+export function updateZodiacConfig(data) {
+  return request({
+    url: '/api/admin/zodiac-config/update',
+    method: 'post',
+    data
+  })
+}
+
+export function deleteZodiacConfig(id) {
+  return request({
+    url: '/api/admin/zodiac-config/delete',
+    method: 'post',
+    data: { id }
+  })
+}
+
+// ========== 八字配置管理 API ==========
+
+export function listBaziConfigs() {
+  return request({
+    url: '/api/admin/bazi-config/list',
+    method: 'post'
+  })
+}
+
+export function createBaziConfig(data) {
+  return request({
+    url: '/api/admin/bazi-config/create',
+    method: 'post',
+    data
+  })
+}
+
+export function updateBaziConfig(data) {
+  return request({
+    url: '/api/admin/bazi-config/update',
+    method: 'post',
+    data
+  })
+}
+
+export function deleteBaziConfig(id) {
+  return request({
+    url: '/api/admin/bazi-config/delete',
+    method: 'post',
+    data: { id }
+  })
+}
+
+// ========== 血型配置管理 API ==========
+
+export function listBloodTypeConfigs() {
+  return request({
+    url: '/api/admin/blood-type-config/list',
+    method: 'post'
+  })
+}
+
+export function createBloodTypeConfig(data) {
+  return request({
+    url: '/api/admin/blood-type-config/create',
+    method: 'post',
+    data
+  })
+}
+
+export function updateBloodTypeConfig(data) {
+  return request({
+    url: '/api/admin/blood-type-config/update',
+    method: 'post',
+    data
+  })
+}
+
+export function deleteBloodTypeConfig(id) {
+  return request({
+    url: '/api/admin/blood-type-config/delete',
+    method: 'post',
+    data: { id }
+  })
+}

+ 20 - 1
cfc-web/src/router/index.js

@@ -240,6 +240,24 @@ const routes = [
         name: 'EducationSystems',
         component: () => import('@/views/admin/EducationSystems.vue'),
         meta: { title: '教育体系管理' }
+      },
+      {
+        path: 'zodiac-configs',
+        name: 'ZodiacConfigs',
+        component: () => import('@/views/admin/ZodiacConfigs.vue'),
+        meta: { title: '星座配置' }
+      },
+      {
+        path: 'bazi-configs',
+        name: 'BaziConfigs',
+        component: () => import('@/views/admin/BaziConfigs.vue'),
+        meta: { title: '八字配置' }
+      },
+      {
+        path: 'blood-type-configs',
+        name: 'BloodTypeConfigs',
+        component: () => import('@/views/admin/BloodTypeConfigs.vue'),
+        meta: { title: '血型配置' }
       }
     ]
   }
@@ -331,7 +349,8 @@ router.beforeEach(async (to, from, next) => {
     'AssessmentAdmin', 'VendorReview',
     'ProductManage', 'OrderManage', 'SysConfig', 'EnergySandbox',
     'ArticleCategory', 'ArticleManage', 'ArticleEdit',
-    'EducationSystems'
+    'EducationSystems',
+    'ZodiacConfigs', 'BaziConfigs', 'BloodTypeConfigs'
   ]
   if (adminRoutes.includes(to.name) && role === 'teacher') {
     console.warn('Access denied: Admin role required for', to.name)

+ 12 - 0
cfc-web/src/views/Layout.vue

@@ -185,6 +185,18 @@
             <i class="el-icon-s-management"></i>
             <span>教育体系</span>
           </el-menu-item>
+          <el-menu-item index="/zodiac-configs">
+            <i class="el-icon-s-management"></i>
+            <span>星座配置</span>
+          </el-menu-item>
+          <el-menu-item index="/bazi-configs">
+            <i class="el-icon-s-management"></i>
+            <span>八字配置</span>
+          </el-menu-item>
+          <el-menu-item index="/blood-type-configs">
+            <i class="el-icon-s-management"></i>
+            <span>血型配置</span>
+          </el-menu-item>
         </el-submenu>
         </template>
       </el-menu>

+ 160 - 0
cfc-web/src/views/admin/BaziConfigs.vue

@@ -0,0 +1,160 @@
+<template>
+  <div class="bazi-configs">
+    <el-card>
+      <div slot="header">
+        <span>八字配置管理</span>
+        <el-button style="float: right" type="primary" size="small" @click="handleCreate">+ 添加配置</el-button>
+      </div>
+      <el-table :data="configs" stripe>
+        <el-table-column prop="id" label="ID" width="80"></el-table-column>
+        <el-table-column prop="stemOrBranch" label="天干/地支" width="100"></el-table-column>
+        <el-table-column prop="type" label="类型" width="80">
+          <template slot-scope="scope">
+            <el-tag :type="scope.row.type === 'stem' ? 'primary' : 'success'" size="mini">
+              {{ scope.row.type === 'stem' ? '天干' : '地支' }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="element" label="五行"></el-table-column>
+        <el-table-column prop="yinYang" label="阴阳"></el-table-column>
+        <el-table-column prop="mindBase" label="心基础分" width="100"></el-table-column>
+        <el-table-column prop="wisdomBase" label="智基础分" width="100"></el-table-column>
+        <el-table-column label="操作" width="150">
+          <template slot-scope="scope">
+            <el-button size="mini" @click="handleEdit(scope.row)">编辑</el-button>
+            <el-button size="mini" type="danger" @click="handleDelete(scope.row)">删除</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+    </el-card>
+
+    <el-dialog :visible.sync="dialogVisible" :title="isEdit ? '编辑八字配置' : '添加八字配置'" width="500px">
+      <el-form :model="form" label-width="100px">
+        <el-form-item label="天干/地支">
+          <el-input v-model="form.stemOrBranch" placeholder="如: 甲"></el-input>
+        </el-form-item>
+        <el-form-item label="类型">
+          <el-select v-model="form.type" placeholder="选择类型">
+            <el-option label="天干" value="stem"></el-option>
+            <el-option label="地支" value="branch"></el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item label="五行">
+          <el-select v-model="form.element" placeholder="选择五行">
+            <el-option label="金" value="金"></el-option>
+            <el-option label="木" value="木"></el-option>
+            <el-option label="水" value="水"></el-option>
+            <el-option label="火" value="火"></el-option>
+            <el-option label="土" value="土"></el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item label="阴阳">
+          <el-select v-model="form.yinYang" placeholder="选择阴阳">
+            <el-option label="阳" value="yang"></el-option>
+            <el-option label="阴" value="yin"></el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item label="心基础分">
+          <el-input-number v-model="form.mindBase" :min="0" :max="100" :precision="2" style="width:150px"></el-input-number>
+        </el-form-item>
+        <el-form-item label="智基础分">
+          <el-input-number v-model="form.wisdomBase" :min="0" :max="100" :precision="2" style="width:150px"></el-input-number>
+        </el-form-item>
+        <el-form-item label="身修正系数">
+          <el-input-number v-model="form.bodyModifier" :min="0" :max="2" :precision="2" :step="0.05" style="width:150px"></el-input-number>
+        </el-form-item>
+        <el-form-item label="行修正系数">
+          <el-input-number v-model="form.actionModifier" :min="0" :max="2" :precision="2" :step="0.05" style="width:150px"></el-input-number>
+        </el-form-item>
+        <el-form-item label="富修正系数">
+          <el-input-number v-model="form.wealthModifier" :min="0" :max="2" :precision="2" :step="0.05" style="width:150px"></el-input-number>
+        </el-form-item>
+      </el-form>
+      <div slot="footer">
+        <el-button @click="dialogVisible = false">取消</el-button>
+        <el-button type="primary" @click="handleSubmit">保存</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { listBaziConfigs, createBaziConfig, updateBaziConfig, deleteBaziConfig } from '@/api/admin'
+
+export default {
+  name: 'BaziConfigs',
+  data() {
+    return {
+      configs: [],
+      dialogVisible: false,
+      isEdit: false,
+      form: this.getEmptyForm()
+    }
+  },
+  created() {
+    this.loadData()
+  },
+  methods: {
+    getEmptyForm() {
+      return {
+        id: null,
+        stemOrBranch: '',
+        type: 'stem',
+        element: '',
+        yinYang: 'yang',
+        mindBase: 60,
+        wisdomBase: 60,
+        bodyModifier: 1.00,
+        actionModifier: 1.00,
+        wealthModifier: 1.00
+      }
+    },
+    async loadData() {
+      try {
+        const res = await listBaziConfigs()
+        if (res.data) this.configs = res.data
+      } catch (e) {
+        console.error(e)
+      }
+    },
+    handleCreate() {
+      this.isEdit = false
+      this.form = this.getEmptyForm()
+      this.dialogVisible = true
+    },
+    handleEdit(row) {
+      this.isEdit = true
+      this.form = { ...row }
+      this.dialogVisible = true
+    },
+    async handleDelete(row) {
+      try {
+        await this.$confirm('确定删除该配置吗?', '提示', { type: 'warning' })
+        await deleteBaziConfig(row.id)
+        this.$message.success('删除成功')
+        this.loadData()
+      } catch (e) {
+        if (e !== 'cancel') this.$message.error('删除失败')
+      }
+    },
+    async handleSubmit() {
+      try {
+        if (this.isEdit) {
+          await updateBaziConfig(this.form)
+        } else {
+          await createBaziConfig(this.form)
+        }
+        this.$message.success('保存成功')
+        this.dialogVisible = false
+        this.loadData()
+      } catch (e) {
+        this.$message.error('保存失败')
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.bazi-configs { padding: 20px; }
+</style>

+ 137 - 0
cfc-web/src/views/admin/BloodTypeConfigs.vue

@@ -0,0 +1,137 @@
+<template>
+  <div class="blood-type-configs">
+    <el-card>
+      <div slot="header">
+        <span>血型配置管理</span>
+        <el-button style="float: right" type="primary" size="small" @click="handleCreate">+ 添加配置</el-button>
+      </div>
+      <el-table :data="configs" stripe>
+        <el-table-column prop="id" label="ID" width="80"></el-table-column>
+        <el-table-column prop="bloodType" label="血型" width="80"></el-table-column>
+        <el-table-column prop="description" label="性格描述" min-width="200"></el-table-column>
+        <el-table-column prop="mindBase" label="心基础分" width="100"></el-table-column>
+        <el-table-column prop="wisdomBase" label="智基础分" width="100"></el-table-column>
+        <el-table-column label="操作" width="150">
+          <template slot-scope="scope">
+            <el-button size="mini" @click="handleEdit(scope.row)">编辑</el-button>
+            <el-button size="mini" type="danger" @click="handleDelete(scope.row)">删除</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+    </el-card>
+
+    <el-dialog :visible.sync="dialogVisible" :title="isEdit ? '编辑血型配置' : '添加血型配置'" width="500px">
+      <el-form :model="form" label-width="100px">
+        <el-form-item label="血型">
+          <el-select v-model="form.bloodType" placeholder="选择血型">
+            <el-option label="A型" value="A"></el-option>
+            <el-option label="B型" value="B"></el-option>
+            <el-option label="AB型" value="AB"></el-option>
+            <el-option label="O型" value="O"></el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item label="性格描述">
+          <el-input v-model="form.description" type="textarea" :rows="3"></el-input>
+        </el-form-item>
+        <el-form-item label="心基础分">
+          <el-input-number v-model="form.mindBase" :min="0" :max="100" :precision="2" style="width:150px"></el-input-number>
+        </el-form-item>
+        <el-form-item label="智基础分">
+          <el-input-number v-model="form.wisdomBase" :min="0" :max="100" :precision="2" style="width:150px"></el-input-number>
+        </el-form-item>
+        <el-form-item label="身修正系数">
+          <el-input-number v-model="form.bodyModifier" :min="0" :max="2" :precision="2" :step="0.05" style="width:150px"></el-input-number>
+        </el-form-item>
+        <el-form-item label="行修正系数">
+          <el-input-number v-model="form.actionModifier" :min="0" :max="2" :precision="2" :step="0.05" style="width:150px"></el-input-number>
+        </el-form-item>
+        <el-form-item label="富修正系数">
+          <el-input-number v-model="form.wealthModifier" :min="0" :max="2" :precision="2" :step="0.05" style="width:150px"></el-input-number>
+        </el-form-item>
+      </el-form>
+      <div slot="footer">
+        <el-button @click="dialogVisible = false">取消</el-button>
+        <el-button type="primary" @click="handleSubmit">保存</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { listBloodTypeConfigs, createBloodTypeConfig, updateBloodTypeConfig, deleteBloodTypeConfig } from '@/api/admin'
+
+export default {
+  name: 'BloodTypeConfigs',
+  data() {
+    return {
+      configs: [],
+      dialogVisible: false,
+      isEdit: false,
+      form: this.getEmptyForm()
+    }
+  },
+  created() {
+    this.loadData()
+  },
+  methods: {
+    getEmptyForm() {
+      return {
+        id: null,
+        bloodType: 'A',
+        description: '',
+        mindBase: 60,
+        wisdomBase: 60,
+        bodyModifier: 1.00,
+        actionModifier: 1.00,
+        wealthModifier: 1.00
+      }
+    },
+    async loadData() {
+      try {
+        const res = await listBloodTypeConfigs()
+        if (res.data) this.configs = res.data
+      } catch (e) {
+        console.error(e)
+      }
+    },
+    handleCreate() {
+      this.isEdit = false
+      this.form = this.getEmptyForm()
+      this.dialogVisible = true
+    },
+    handleEdit(row) {
+      this.isEdit = true
+      this.form = { ...row }
+      this.dialogVisible = true
+    },
+    async handleDelete(row) {
+      try {
+        await this.$confirm('确定删除该配置吗?', '提示', { type: 'warning' })
+        await deleteBloodTypeConfig(row.id)
+        this.$message.success('删除成功')
+        this.loadData()
+      } catch (e) {
+        if (e !== 'cancel') this.$message.error('删除失败')
+      }
+    },
+    async handleSubmit() {
+      try {
+        if (this.isEdit) {
+          await updateBloodTypeConfig(this.form)
+        } else {
+          await createBloodTypeConfig(this.form)
+        }
+        this.$message.success('保存成功')
+        this.dialogVisible = false
+        this.loadData()
+      } catch (e) {
+        this.$message.error('保存失败')
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.blood-type-configs { padding: 20px; }
+</style>

+ 160 - 0
cfc-web/src/views/admin/ZodiacConfigs.vue

@@ -0,0 +1,160 @@
+<template>
+  <div class="zodiac-configs">
+    <el-card>
+      <div slot="header">
+        <span>星座配置管理</span>
+        <el-button style="float: right" type="primary" size="small" @click="handleCreate">+ 添加配置</el-button>
+      </div>
+      <el-table :data="configs" stripe>
+        <el-table-column prop="id" label="ID" width="80"></el-table-column>
+        <el-table-column prop="zodiacCode" label="代码" width="120"></el-table-column>
+        <el-table-column prop="zodiacName" label="名称"></el-table-column>
+        <el-table-column prop="element" label="五行"></el-table-column>
+        <el-table-column label="日期范围">
+          <template slot-scope="scope">
+            {{ scope.row.startMonth }}.{{ scope.row.startDay }} - {{ scope.row.endMonth }}.{{ scope.row.endDay }}
+          </template>
+        </el-table-column>
+        <el-table-column prop="mindBase" label="心基础分" width="100"></el-table-column>
+        <el-table-column prop="wisdomBase" label="智基础分" width="100"></el-table-column>
+        <el-table-column label="操作" width="150">
+          <template slot-scope="scope">
+            <el-button size="mini" @click="handleEdit(scope.row)">编辑</el-button>
+            <el-button size="mini" type="danger" @click="handleDelete(scope.row)">删除</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+    </el-card>
+
+    <el-dialog :visible.sync="dialogVisible" :title="isEdit ? '编辑星座配置' : '添加星座配置'" width="600px">
+      <el-form :model="form" label-width="100px">
+        <el-form-item label="代码">
+          <el-input v-model="form.zodiacCode" placeholder="如: aries"></el-input>
+        </el-form-item>
+        <el-form-item label="名称">
+          <el-input v-model="form.zodiacName" placeholder="如: 白羊座"></el-input>
+        </el-form-item>
+        <el-form-item label="五行">
+          <el-select v-model="form.element" placeholder="选择五行">
+            <el-option label="金" value="金"></el-option>
+            <el-option label="木" value="木"></el-option>
+            <el-option label="水" value="水"></el-option>
+            <el-option label="火" value="火"></el-option>
+            <el-option label="土" value="土"></el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item label="起始日期">
+          <el-input-number v-model="form.startMonth" :min="1" :max="12" style="width:100px"></el-input-number> 月
+          <el-input-number v-model="form.startDay" :min="1" :max="31" style="width:100px"></el-input-number> 日
+        </el-form-item>
+        <el-form-item label="结束日期">
+          <el-input-number v-model="form.endMonth" :min="1" :max="12" style="width:100px"></el-input-number> 月
+          <el-input-number v-model="form.endDay" :min="1" :max="31" style="width:100px"></el-input-number> 日
+        </el-form-item>
+        <el-form-item label="心基础分">
+          <el-input-number v-model="form.mindBase" :min="0" :max="100" :precision="2" style="width:150px"></el-input-number>
+        </el-form-item>
+        <el-form-item label="智基础分">
+          <el-input-number v-model="form.wisdomBase" :min="0" :max="100" :precision="2" style="width:150px"></el-input-number>
+        </el-form-item>
+        <el-form-item label="身修正系数">
+          <el-input-number v-model="form.bodyModifier" :min="0" :max="2" :precision="2" :step="0.05" style="width:150px"></el-input-number>
+        </el-form-item>
+        <el-form-item label="行修正系数">
+          <el-input-number v-model="form.actionModifier" :min="0" :max="2" :precision="2" :step="0.05" style="width:150px"></el-input-number>
+        </el-form-item>
+        <el-form-item label="富修正系数">
+          <el-input-number v-model="form.wealthModifier" :min="0" :max="2" :precision="2" :step="0.05" style="width:150px"></el-input-number>
+        </el-form-item>
+      </el-form>
+      <div slot="footer">
+        <el-button @click="dialogVisible = false">取消</el-button>
+        <el-button type="primary" @click="handleSubmit">保存</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { listZodiacConfigs, createZodiacConfig, updateZodiacConfig, deleteZodiacConfig } from '@/api/admin'
+
+export default {
+  name: 'ZodiacConfigs',
+  data() {
+    return {
+      configs: [],
+      dialogVisible: false,
+      isEdit: false,
+      form: this.getEmptyForm()
+    }
+  },
+  created() {
+    this.loadData()
+  },
+  methods: {
+    getEmptyForm() {
+      return {
+        id: null,
+        zodiacCode: '',
+        zodiacName: '',
+        element: '',
+        startMonth: 1,
+        startDay: 1,
+        endMonth: 12,
+        endDay: 31,
+        mindBase: 60,
+        wisdomBase: 60,
+        bodyModifier: 1.00,
+        actionModifier: 1.00,
+        wealthModifier: 1.00
+      }
+    },
+    async loadData() {
+      try {
+        const res = await listZodiacConfigs()
+        if (res.data) this.configs = res.data
+      } catch (e) {
+        console.error(e)
+      }
+    },
+    handleCreate() {
+      this.isEdit = false
+      this.form = this.getEmptyForm()
+      this.dialogVisible = true
+    },
+    handleEdit(row) {
+      this.isEdit = true
+      this.form = { ...row }
+      this.dialogVisible = true
+    },
+    async handleDelete(row) {
+      try {
+        await this.$confirm('确定删除该配置吗?', '提示', { type: 'warning' })
+        await deleteZodiacConfig(row.id)
+        this.$message.success('删除成功')
+        this.loadData()
+      } catch (e) {
+        if (e !== 'cancel') this.$message.error('删除失败')
+      }
+    },
+    async handleSubmit() {
+      try {
+        if (this.isEdit) {
+          await updateZodiacConfig(this.form)
+        } else {
+          await createZodiacConfig(this.form)
+        }
+        this.$message.success('保存成功')
+        this.dialogVisible = false
+        this.loadData()
+      } catch (e) {
+        this.$message.error('保存失败')
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.zodiac-configs { padding: 20px; }
+</style>

+ 57 - 0
dan/danshop/.classpath

@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+	<classpathentry kind="src" output="target/classes" path="src/main/java">
+		<attributes>
+			<attribute name="optional" value="true"/>
+			<attribute name="maven.pomderived" value="true"/>
+		</attributes>
+	</classpathentry>
+	<classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources">
+		<attributes>
+			<attribute name="maven.pomderived" value="true"/>
+			<attribute name="optional" value="true"/>
+		</attributes>
+	</classpathentry>
+	<classpathentry kind="src" output="target/test-classes" path="src/test/java">
+		<attributes>
+			<attribute name="optional" value="true"/>
+			<attribute name="maven.pomderived" value="true"/>
+			<attribute name="test" value="true"/>
+		</attributes>
+	</classpathentry>
+	<classpathentry excluding="**" kind="src" output="target/test-classes" path="src/test/resources">
+		<attributes>
+			<attribute name="maven.pomderived" value="true"/>
+			<attribute name="test" value="true"/>
+			<attribute name="optional" value="true"/>
+		</attributes>
+	</classpathentry>
+	<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8">
+		<attributes>
+			<attribute name="maven.pomderived" value="true"/>
+		</attributes>
+	</classpathentry>
+	<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
+		<attributes>
+			<attribute name="maven.pomderived" value="true"/>
+		</attributes>
+	</classpathentry>
+	<classpathentry kind="src" path="target/generated-sources/annotations">
+		<attributes>
+			<attribute name="optional" value="true"/>
+			<attribute name="maven.pomderived" value="true"/>
+			<attribute name="ignore_optional_problems" value="true"/>
+			<attribute name="m2e-apt" value="true"/>
+		</attributes>
+	</classpathentry>
+	<classpathentry kind="src" output="target/test-classes" path="target/generated-test-sources/test-annotations">
+		<attributes>
+			<attribute name="optional" value="true"/>
+			<attribute name="maven.pomderived" value="true"/>
+			<attribute name="ignore_optional_problems" value="true"/>
+			<attribute name="m2e-apt" value="true"/>
+			<attribute name="test" value="true"/>
+		</attributes>
+	</classpathentry>
+	<classpathentry kind="output" path="target/classes"/>
+</classpath>

+ 87 - 0
dan/danshop/.factorypath

@@ -0,0 +1,87 @@
+<factorypath>
+    <factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/boot/spring-boot-starter-web/2.7.18/spring-boot-starter-web-2.7.18.jar" enabled="true" runInBatchMode="false"/>
+    <factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/boot/spring-boot-starter/2.7.18/spring-boot-starter-2.7.18.jar" enabled="true" runInBatchMode="false"/>
+    <factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/boot/spring-boot/2.7.18/spring-boot-2.7.18.jar" enabled="true" runInBatchMode="false"/>
+    <factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/boot/spring-boot-starter-logging/2.7.18/spring-boot-starter-logging-2.7.18.jar" enabled="true" runInBatchMode="false"/>
+    <factorypathentry kind="VARJAR" id="M2_REPO/ch/qos/logback/logback-classic/1.2.12/logback-classic-1.2.12.jar" enabled="true" runInBatchMode="false"/>
+    <factorypathentry kind="VARJAR" id="M2_REPO/ch/qos/logback/logback-core/1.2.12/logback-core-1.2.12.jar" enabled="true" runInBatchMode="false"/>
+    <factorypathentry kind="VARJAR" id="M2_REPO/org/apache/logging/log4j/log4j-to-slf4j/2.17.2/log4j-to-slf4j-2.17.2.jar" enabled="true" runInBatchMode="false"/>
+    <factorypathentry kind="VARJAR" id="M2_REPO/org/apache/logging/log4j/log4j-api/2.17.2/log4j-api-2.17.2.jar" enabled="true" runInBatchMode="false"/>
+    <factorypathentry kind="VARJAR" id="M2_REPO/org/slf4j/jul-to-slf4j/1.7.36/jul-to-slf4j-1.7.36.jar" enabled="true" runInBatchMode="false"/>
+    <factorypathentry kind="VARJAR" id="M2_REPO/jakarta/annotation/jakarta.annotation-api/1.3.5/jakarta.annotation-api-1.3.5.jar" enabled="true" runInBatchMode="false"/>
+    <factorypathentry kind="VARJAR" id="M2_REPO/org/yaml/snakeyaml/1.30/snakeyaml-1.30.jar" enabled="true" runInBatchMode="false"/>
+    <factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/boot/spring-boot-starter-json/2.7.18/spring-boot-starter-json-2.7.18.jar" enabled="true" runInBatchMode="false"/>
+    <factorypathentry kind="VARJAR" id="M2_REPO/com/fasterxml/jackson/datatype/jackson-datatype-jdk8/2.13.5/jackson-datatype-jdk8-2.13.5.jar" enabled="true" runInBatchMode="false"/>
+    <factorypathentry kind="VARJAR" id="M2_REPO/com/fasterxml/jackson/datatype/jackson-datatype-jsr310/2.13.5/jackson-datatype-jsr310-2.13.5.jar" enabled="true" runInBatchMode="false"/>
+    <factorypathentry kind="VARJAR" id="M2_REPO/com/fasterxml/jackson/module/jackson-module-parameter-names/2.13.5/jackson-module-parameter-names-2.13.5.jar" enabled="true" runInBatchMode="false"/>
+    <factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/boot/spring-boot-starter-tomcat/2.7.18/spring-boot-starter-tomcat-2.7.18.jar" enabled="true" runInBatchMode="false"/>
+    <factorypathentry kind="VARJAR" id="M2_REPO/org/apache/tomcat/embed/tomcat-embed-core/9.0.83/tomcat-embed-core-9.0.83.jar" enabled="true" runInBatchMode="false"/>
+    <factorypathentry kind="VARJAR" id="M2_REPO/org/apache/tomcat/embed/tomcat-embed-websocket/9.0.83/tomcat-embed-websocket-9.0.83.jar" enabled="true" runInBatchMode="false"/>
+    <factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/spring-web/5.3.31/spring-web-5.3.31.jar" enabled="true" runInBatchMode="false"/>
+    <factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/spring-beans/5.3.31/spring-beans-5.3.31.jar" enabled="true" runInBatchMode="false"/>
+    <factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/spring-webmvc/5.3.31/spring-webmvc-5.3.31.jar" enabled="true" runInBatchMode="false"/>
+    <factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/spring-aop/5.3.31/spring-aop-5.3.31.jar" enabled="true" runInBatchMode="false"/>
+    <factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/spring-context/5.3.31/spring-context-5.3.31.jar" enabled="true" runInBatchMode="false"/>
+    <factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/spring-expression/5.3.31/spring-expression-5.3.31.jar" enabled="true" runInBatchMode="false"/>
+    <factorypathentry kind="VARJAR" id="M2_REPO/com/baomidou/mybatis-plus-boot-starter/3.5.3.1/mybatis-plus-boot-starter-3.5.3.1.jar" enabled="true" runInBatchMode="false"/>
+    <factorypathentry kind="VARJAR" id="M2_REPO/com/baomidou/mybatis-plus/3.5.3.1/mybatis-plus-3.5.3.1.jar" enabled="true" runInBatchMode="false"/>
+    <factorypathentry kind="VARJAR" id="M2_REPO/com/baomidou/mybatis-plus-extension/3.5.3.1/mybatis-plus-extension-3.5.3.1.jar" enabled="true" runInBatchMode="false"/>
+    <factorypathentry kind="VARJAR" id="M2_REPO/com/baomidou/mybatis-plus-core/3.5.3.1/mybatis-plus-core-3.5.3.1.jar" enabled="true" runInBatchMode="false"/>
+    <factorypathentry kind="VARJAR" id="M2_REPO/com/baomidou/mybatis-plus-annotation/3.5.3.1/mybatis-plus-annotation-3.5.3.1.jar" enabled="true" runInBatchMode="false"/>
+    <factorypathentry kind="VARJAR" id="M2_REPO/com/github/jsqlparser/jsqlparser/4.4/jsqlparser-4.4.jar" enabled="true" runInBatchMode="false"/>
+    <factorypathentry kind="VARJAR" id="M2_REPO/org/mybatis/mybatis/3.5.10/mybatis-3.5.10.jar" enabled="true" runInBatchMode="false"/>
+    <factorypathentry kind="VARJAR" id="M2_REPO/org/mybatis/mybatis-spring/2.0.7/mybatis-spring-2.0.7.jar" enabled="true" runInBatchMode="false"/>
+    <factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/boot/spring-boot-autoconfigure/2.7.18/spring-boot-autoconfigure-2.7.18.jar" enabled="true" runInBatchMode="false"/>
+    <factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/boot/spring-boot-starter-jdbc/2.7.18/spring-boot-starter-jdbc-2.7.18.jar" enabled="true" runInBatchMode="false"/>
+    <factorypathentry kind="VARJAR" id="M2_REPO/com/zaxxer/HikariCP/4.0.3/HikariCP-4.0.3.jar" enabled="true" runInBatchMode="false"/>
+    <factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/spring-jdbc/5.3.31/spring-jdbc-5.3.31.jar" enabled="true" runInBatchMode="false"/>
+    <factorypathentry kind="VARJAR" id="M2_REPO/com/mysql/mysql-connector-j/8.0.33/mysql-connector-j-8.0.33.jar" enabled="true" runInBatchMode="false"/>
+    <factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/boot/spring-boot-starter-data-redis/2.7.18/spring-boot-starter-data-redis-2.7.18.jar" enabled="true" runInBatchMode="false"/>
+    <factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/data/spring-data-redis/2.7.18/spring-data-redis-2.7.18.jar" enabled="true" runInBatchMode="false"/>
+    <factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/data/spring-data-keyvalue/2.7.18/spring-data-keyvalue-2.7.18.jar" enabled="true" runInBatchMode="false"/>
+    <factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/data/spring-data-commons/2.7.18/spring-data-commons-2.7.18.jar" enabled="true" runInBatchMode="false"/>
+    <factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/spring-tx/5.3.31/spring-tx-5.3.31.jar" enabled="true" runInBatchMode="false"/>
+    <factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/spring-oxm/5.3.31/spring-oxm-5.3.31.jar" enabled="true" runInBatchMode="false"/>
+    <factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/spring-context-support/5.3.31/spring-context-support-5.3.31.jar" enabled="true" runInBatchMode="false"/>
+    <factorypathentry kind="VARJAR" id="M2_REPO/org/slf4j/slf4j-api/1.7.36/slf4j-api-1.7.36.jar" enabled="true" runInBatchMode="false"/>
+    <factorypathentry kind="VARJAR" id="M2_REPO/io/lettuce/lettuce-core/6.1.10.RELEASE/lettuce-core-6.1.10.RELEASE.jar" enabled="true" runInBatchMode="false"/>
+    <factorypathentry kind="VARJAR" id="M2_REPO/io/netty/netty-common/4.1.101.Final/netty-common-4.1.101.Final.jar" enabled="true" runInBatchMode="false"/>
+    <factorypathentry kind="VARJAR" id="M2_REPO/io/netty/netty-handler/4.1.101.Final/netty-handler-4.1.101.Final.jar" enabled="true" runInBatchMode="false"/>
+    <factorypathentry kind="VARJAR" id="M2_REPO/io/netty/netty-resolver/4.1.101.Final/netty-resolver-4.1.101.Final.jar" enabled="true" runInBatchMode="false"/>
+    <factorypathentry kind="VARJAR" id="M2_REPO/io/netty/netty-buffer/4.1.101.Final/netty-buffer-4.1.101.Final.jar" enabled="true" runInBatchMode="false"/>
+    <factorypathentry kind="VARJAR" id="M2_REPO/io/netty/netty-transport-native-unix-common/4.1.101.Final/netty-transport-native-unix-common-4.1.101.Final.jar" enabled="true" runInBatchMode="false"/>
+    <factorypathentry kind="VARJAR" id="M2_REPO/io/netty/netty-codec/4.1.101.Final/netty-codec-4.1.101.Final.jar" enabled="true" runInBatchMode="false"/>
+    <factorypathentry kind="VARJAR" id="M2_REPO/io/netty/netty-transport/4.1.101.Final/netty-transport-4.1.101.Final.jar" enabled="true" runInBatchMode="false"/>
+    <factorypathentry kind="VARJAR" id="M2_REPO/io/projectreactor/reactor-core/3.4.34/reactor-core-3.4.34.jar" enabled="true" runInBatchMode="false"/>
+    <factorypathentry kind="VARJAR" id="M2_REPO/org/reactivestreams/reactive-streams/1.0.4/reactive-streams-1.0.4.jar" enabled="true" runInBatchMode="false"/>
+    <factorypathentry kind="VARJAR" id="M2_REPO/org/apache/commons/commons-pool2/2.11.1/commons-pool2-2.11.1.jar" enabled="true" runInBatchMode="false"/>
+    <factorypathentry kind="VARJAR" id="M2_REPO/io/jsonwebtoken/jjwt-api/0.11.5/jjwt-api-0.11.5.jar" enabled="true" runInBatchMode="false"/>
+    <factorypathentry kind="VARJAR" id="M2_REPO/io/jsonwebtoken/jjwt-impl/0.11.5/jjwt-impl-0.11.5.jar" enabled="true" runInBatchMode="false"/>
+    <factorypathentry kind="VARJAR" id="M2_REPO/io/jsonwebtoken/jjwt-jackson/0.11.5/jjwt-jackson-0.11.5.jar" enabled="true" runInBatchMode="false"/>
+    <factorypathentry kind="VARJAR" id="M2_REPO/com/fasterxml/jackson/core/jackson-databind/2.13.5/jackson-databind-2.13.5.jar" enabled="true" runInBatchMode="false"/>
+    <factorypathentry kind="VARJAR" id="M2_REPO/com/fasterxml/jackson/core/jackson-annotations/2.13.5/jackson-annotations-2.13.5.jar" enabled="true" runInBatchMode="false"/>
+    <factorypathentry kind="VARJAR" id="M2_REPO/com/fasterxml/jackson/core/jackson-core/2.13.5/jackson-core-2.13.5.jar" enabled="true" runInBatchMode="false"/>
+    <factorypathentry kind="VARJAR" id="M2_REPO/com/squareup/okhttp3/okhttp/3.14.9/okhttp-3.14.9.jar" enabled="true" runInBatchMode="false"/>
+    <factorypathentry kind="VARJAR" id="M2_REPO/com/squareup/okio/okio/1.17.2/okio-1.17.2.jar" enabled="true" runInBatchMode="false"/>
+    <factorypathentry kind="VARJAR" id="M2_REPO/com/alibaba/fastjson2/fastjson2/2.0.40/fastjson2-2.0.40.jar" enabled="true" runInBatchMode="false"/>
+    <factorypathentry kind="VARJAR" id="M2_REPO/org/projectlombok/lombok/1.18.30/lombok-1.18.30.jar" enabled="true" runInBatchMode="false"/>
+    <factorypathentry kind="VARJAR" id="M2_REPO/org/apache/commons/commons-lang3/3.12.0/commons-lang3-3.12.0.jar" enabled="true" runInBatchMode="false"/>
+    <factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/boot/spring-boot-starter-validation/2.7.18/spring-boot-starter-validation-2.7.18.jar" enabled="true" runInBatchMode="false"/>
+    <factorypathentry kind="VARJAR" id="M2_REPO/org/apache/tomcat/embed/tomcat-embed-el/9.0.83/tomcat-embed-el-9.0.83.jar" enabled="true" runInBatchMode="false"/>
+    <factorypathentry kind="VARJAR" id="M2_REPO/org/hibernate/validator/hibernate-validator/6.2.5.Final/hibernate-validator-6.2.5.Final.jar" enabled="true" runInBatchMode="false"/>
+    <factorypathentry kind="VARJAR" id="M2_REPO/jakarta/validation/jakarta.validation-api/2.0.2/jakarta.validation-api-2.0.2.jar" enabled="true" runInBatchMode="false"/>
+    <factorypathentry kind="VARJAR" id="M2_REPO/org/jboss/logging/jboss-logging/3.4.3.Final/jboss-logging-3.4.3.Final.jar" enabled="true" runInBatchMode="false"/>
+    <factorypathentry kind="VARJAR" id="M2_REPO/com/fasterxml/classmate/1.5.1/classmate-1.5.1.jar" enabled="true" runInBatchMode="false"/>
+    <factorypathentry kind="VARJAR" id="M2_REPO/org/springdoc/springdoc-openapi-ui/1.8.0/springdoc-openapi-ui-1.8.0.jar" enabled="true" runInBatchMode="false"/>
+    <factorypathentry kind="VARJAR" id="M2_REPO/org/springdoc/springdoc-openapi-webmvc-core/1.8.0/springdoc-openapi-webmvc-core-1.8.0.jar" enabled="true" runInBatchMode="false"/>
+    <factorypathentry kind="VARJAR" id="M2_REPO/org/springdoc/springdoc-openapi-common/1.8.0/springdoc-openapi-common-1.8.0.jar" enabled="true" runInBatchMode="false"/>
+    <factorypathentry kind="VARJAR" id="M2_REPO/io/swagger/core/v3/swagger-core/2.2.20/swagger-core-2.2.20.jar" enabled="true" runInBatchMode="false"/>
+    <factorypathentry kind="VARJAR" id="M2_REPO/com/fasterxml/jackson/dataformat/jackson-dataformat-yaml/2.13.5/jackson-dataformat-yaml-2.13.5.jar" enabled="true" runInBatchMode="false"/>
+    <factorypathentry kind="VARJAR" id="M2_REPO/io/swagger/core/v3/swagger-annotations/2.2.20/swagger-annotations-2.2.20.jar" enabled="true" runInBatchMode="false"/>
+    <factorypathentry kind="VARJAR" id="M2_REPO/io/swagger/core/v3/swagger-models/2.2.20/swagger-models-2.2.20.jar" enabled="true" runInBatchMode="false"/>
+    <factorypathentry kind="VARJAR" id="M2_REPO/org/webjars/swagger-ui/5.11.8/swagger-ui-5.11.8.jar" enabled="true" runInBatchMode="false"/>
+    <factorypathentry kind="VARJAR" id="M2_REPO/jakarta/xml/bind/jakarta.xml.bind-api/2.3.3/jakarta.xml.bind-api-2.3.3.jar" enabled="true" runInBatchMode="false"/>
+    <factorypathentry kind="VARJAR" id="M2_REPO/jakarta/activation/jakarta.activation-api/1.2.2/jakarta.activation-api-1.2.2.jar" enabled="true" runInBatchMode="false"/>
+    <factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/spring-core/5.3.31/spring-core-5.3.31.jar" enabled="true" runInBatchMode="false"/>
+    <factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/spring-jcl/5.3.31/spring-jcl-5.3.31.jar" enabled="true" runInBatchMode="false"/>
+</factorypath>

+ 34 - 0
dan/danshop/.project

@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+	<name>danshop</name>
+	<comment></comment>
+	<projects>
+	</projects>
+	<buildSpec>
+		<buildCommand>
+			<name>org.eclipse.jdt.core.javabuilder</name>
+			<arguments>
+			</arguments>
+		</buildCommand>
+		<buildCommand>
+			<name>org.eclipse.m2e.core.maven2Builder</name>
+			<arguments>
+			</arguments>
+		</buildCommand>
+	</buildSpec>
+	<natures>
+		<nature>org.eclipse.jdt.core.javanature</nature>
+		<nature>org.eclipse.m2e.core.maven2Nature</nature>
+	</natures>
+	<filteredResources>
+		<filter>
+			<id>1781272892739</id>
+			<name></name>
+			<type>30</type>
+			<matcher>
+				<id>org.eclipse.core.resources.regexFilterMatcher</id>
+				<arguments>node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__</arguments>
+			</matcher>
+		</filter>
+	</filteredResources>
+</projectDescription>

+ 4 - 0
dan/danshop/.settings/org.eclipse.core.resources.prefs

@@ -0,0 +1,4 @@
+eclipse.preferences.version=1
+encoding//src/main/java=UTF-8
+encoding//src/main/resources=UTF-8
+encoding/<project>=UTF-8

+ 4 - 0
dan/danshop/.settings/org.eclipse.jdt.apt.core.prefs

@@ -0,0 +1,4 @@
+eclipse.preferences.version=1
+org.eclipse.jdt.apt.aptEnabled=true
+org.eclipse.jdt.apt.genSrcDir=target/generated-sources/annotations
+org.eclipse.jdt.apt.genTestSrcDir=target/generated-test-sources/test-annotations

+ 10 - 0
dan/danshop/.settings/org.eclipse.jdt.core.prefs

@@ -0,0 +1,10 @@
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.codegen.methodParameters=generate
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8
+org.eclipse.jdt.core.compiler.compliance=1.8
+org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled
+org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
+org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=ignore
+org.eclipse.jdt.core.compiler.processAnnotations=enabled
+org.eclipse.jdt.core.compiler.release=disabled
+org.eclipse.jdt.core.compiler.source=1.8

+ 4 - 0
dan/danshop/.settings/org.eclipse.m2e.core.prefs

@@ -0,0 +1,4 @@
+activeProfiles=
+eclipse.preferences.version=1
+resolveWorkspaceProjects=true
+version=1

+ 142 - 0
dan/danshop/pom.xml

@@ -0,0 +1,142 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.springframework.boot</groupId>
+        <artifactId>spring-boot-starter-parent</artifactId>
+        <version>2.7.18</version>
+        <relativePath/>
+    </parent>
+
+    <groupId>com.etotem</groupId>
+    <artifactId>danshop</artifactId>
+    <version>1.0.0</version>
+    <packaging>jar</packaging>
+    <name>DanShop</name>
+    <description>浠艾福电商模块 - DanShop E-commerce Module</description>
+
+    <properties>
+        <java.version>1.8</java.version>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <mybatis-plus.version>3.5.3.1</mybatis-plus.version>
+        <fastjson2.version>2.0.40</fastjson2.version>
+    </properties>
+
+    <dependencies>
+        <!-- Spring Boot Web -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+        </dependency>
+
+        <!-- MyBatis-Plus -->
+        <dependency>
+            <groupId>com.baomidou</groupId>
+            <artifactId>mybatis-plus-boot-starter</artifactId>
+            <version>${mybatis-plus.version}</version>
+        </dependency>
+
+        <!-- MySQL -->
+        <dependency>
+            <groupId>mysql</groupId>
+            <artifactId>mysql-connector-java</artifactId>
+            <version>8.0.33</version>
+        </dependency>
+
+        <!-- Redis -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-data-redis</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-pool2</artifactId>
+        </dependency>
+
+        <!-- JWT -->
+        <dependency>
+            <groupId>io.jsonwebtoken</groupId>
+            <artifactId>jjwt-api</artifactId>
+            <version>0.11.5</version>
+        </dependency>
+        <dependency>
+            <groupId>io.jsonwebtoken</groupId>
+            <artifactId>jjwt-impl</artifactId>
+            <version>0.11.5</version>
+            <scope>runtime</scope>
+        </dependency>
+        <dependency>
+            <groupId>io.jsonwebtoken</groupId>
+            <artifactId>jjwt-jackson</artifactId>
+            <version>0.11.5</version>
+            <scope>runtime</scope>
+        </dependency>
+
+        <!-- OkHttp for calling cfc-backend -->
+        <dependency>
+            <groupId>com.squareup.okhttp3</groupId>
+            <artifactId>okhttp</artifactId>
+            <version>3.14.9</version>
+        </dependency>
+
+        <!-- FastJSON2 -->
+        <dependency>
+            <groupId>com.alibaba.fastjson2</groupId>
+            <artifactId>fastjson2</artifactId>
+            <version>${fastjson2.version}</version>
+        </dependency>
+
+        <!-- Lombok -->
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+            <optional>true</optional>
+        </dependency>
+
+        <!-- Commons Lang3 -->
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-lang3</artifactId>
+        </dependency>
+
+        <!-- Validation -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-validation</artifactId>
+        </dependency>
+
+        <!-- SpringDoc OpenAPI -->
+        <dependency>
+            <groupId>org.springdoc</groupId>
+            <artifactId>springdoc-openapi-ui</artifactId>
+            <version>1.8.0</version>
+        </dependency>
+
+        <!-- Test -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+                <configuration>
+                    <excludes>
+                        <exclude>
+                            <groupId>org.projectlombok</groupId>
+                            <artifactId>lombok</artifactId>
+                        </exclude>
+                    </excludes>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+</project>

+ 16 - 0
dan/danshop/src/main/java/com/etotem/danshop/DanShopApplication.java

@@ -0,0 +1,16 @@
+package com.etotem.danshop;
+
+import org.mybatis.spring.annotation.MapperScan;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.scheduling.annotation.EnableScheduling;
+
+@SpringBootApplication(scanBasePackages = "com.etotem.danshop")
+@MapperScan("com.etotem.danshop.mapper")
+@EnableScheduling
+public class DanShopApplication {
+
+    public static void main(String[] args) {
+        SpringApplication.run(DanShopApplication.class, args);
+    }
+}

+ 72 - 0
dan/danshop/src/main/java/com/etotem/danshop/common/DanShopConstants.java

@@ -0,0 +1,72 @@
+package com.etotem.danshop.common;
+
+/**
+ * DanShop 常量
+ */
+public class DanShopConstants {
+
+    // ========== 商品状态 ==========
+    public static final String PRODUCT_STATUS_PENDING = "pending";       // 待审核
+    public static final String PRODUCT_STATUS_APPROVED = "approved";     // 已通过
+    public static final String PRODUCT_STATUS_REJECTED = "rejected";      // 已拒绝
+    public static final String PRODUCT_STATUS_ON_SHELF = "on_shelf";       // 上架
+    public static final String PRODUCT_STATUS_OFF_SHELF = "off_shelf";    // 下架
+
+    // ========== 商品类型 ==========
+    public static final String PRODUCT_TYPE_PHYSICAL = "physical";       // 实物商品
+    public static final String PRODUCT_TYPE_VIRTUAL = "virtual";          // 虚拟商品
+    public static final String PRODUCT_TYPE_COUPON = "coupon";           // 优惠券
+    public static final String PRODUCT_TYPE_COURSE = "course";           // 课程
+
+    // ========== 订单状态 ==========
+    public static final String ORDER_STATUS_PENDING = "pending";           // 待支付
+    public static final String ORDER_STATUS_PAID = "paid";                 // 已支付(待发货)
+    public static final String ORDER_STATUS_SHIPPED = "shipped";           // 已发货(待收货)
+    public static final String ORDER_STATUS_COMPLETED = "completed";       // 已完成
+    public static final String ORDER_STATUS_REFUNDING = "refunding";       // 退款中
+    public static final String ORDER_STATUS_REFUNDED = "refunded";        // 已退款
+    public static final String ORDER_STATUS_CANCELLED = "cancelled";      // 已取消
+
+    // ========== 物流状态 ==========
+    public static final String LOGISTICS_STATUS_PENDING = "pending";       // 待发货
+    public static final String LOGISTICS_STATUS_SHIPPED = "shipped";       // 运输中
+    public static final String LOGISTICS_STATUS_SIGNING = "signing";      // 签收中
+    public static final String LOGISTICS_STATUS_COMPLETED = "completed";  // 已签收
+
+    // ========== 物流公司编码 ==========
+    public static final String LOGISTICS_SF = "sf";     // 顺丰
+    public static final String LOGISTICS_YTO = "yto"; // 圆通
+    public static final String LOGISTICS_ZT = "zt";    // 中通
+    public static final String LOGISTICS_STO = "sto";  // 申通
+    public static final String LOGISTICS_YD = "yd";    // 韵达
+    public static final String LOGISTICS_TTKD = "ttkd"; // 天天快递
+
+    // ========== 订单操作 ==========
+    public static final String ORDER_ACTION_CREATE = "create";
+    public static final String ORDER_ACTION_PAY = "pay";
+    public static final String ORDER_ACTION_SHIP = "ship";
+    public static final String ORDER_ACTION_CONFIRM = "confirm";
+    public static final String ORDER_ACTION_REFUND = "refund";
+    public static final String ORDER_ACTION_CANCEL = "cancel";
+
+    // ========== 操作者类型 ==========
+    public static final String OPERATOR_TYPE_BUYER = "buyer";
+    public static final String OPERATOR_TYPE_VENDOR = "vendor";
+    public static final String OPERATOR_TYPE_ADMIN = "admin";
+    public static final String OPERATOR_TYPE_SYSTEM = "system";
+
+    // ========== Redis Key 前缀 ==========
+    public static final String REDIS_CART_PREFIX = "danshop:cart:";
+
+    // ========== 分润规则类型 ==========
+    public static final String PROFIT_RULE_GLOBAL = "global";
+    public static final String PROFIT_RULE_CATEGORY = "product_category";
+    public static final String PROFIT_RULE_VENDOR = "vendor_level";
+
+    // ========== 支付方式 ==========
+    public static final String PAYMENT_METHOD_WECHAT = "wechat";
+    public static final String PAYMENT_METHOD_ALIPAY = "alipay";
+
+    // ========== 能量来源 ==========
+    public static final String ENERGY_SOURCE_PRODUCT = "danshop_product";
+}

+ 50 - 0
dan/danshop/src/main/java/com/etotem/danshop/common/Result.java

@@ -0,0 +1,50 @@
+package com.etotem.danshop.common;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+@Data
+public class Result<T> implements Serializable {
+
+    private Integer code;
+    private String message;
+    private T data;
+
+    public static <T> Result<T> success() {
+        Result<T> result = new Result<>();
+        result.setCode(200);
+        result.setMessage("success");
+        return result;
+    }
+
+    public static <T> Result<T> success(T data) {
+        Result<T> result = new Result<>();
+        result.setCode(200);
+        result.setMessage("success");
+        result.setData(data);
+        return result;
+    }
+
+    public static <T> Result<T> success(String message, T data) {
+        Result<T> result = new Result<>();
+        result.setCode(200);
+        result.setMessage(message);
+        result.setData(data);
+        return result;
+    }
+
+    public static <T> Result<T> error(String message) {
+        Result<T> result = new Result<>();
+        result.setCode(500);
+        result.setMessage(message);
+        return result;
+    }
+
+    public static <T> Result<T> error(Integer code, String message) {
+        Result<T> result = new Result<>();
+        result.setCode(code);
+        result.setMessage(message);
+        return result;
+    }
+}

+ 76 - 0
dan/danshop/src/main/java/com/etotem/danshop/config/CfcBackendClient.java

@@ -0,0 +1,76 @@
+package com.etotem.danshop.config;
+
+import okhttp3.*;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+import com.alibaba.fastjson.JSONObject;
+import java.io.IOException;
+
+@Component
+public class CfcBackendClient {
+
+    private static final MediaType JSON_TYPE = MediaType.parse("application/json; charset=utf-8");
+    private static final OkHttpClient CLIENT = new OkHttpClient();
+
+    @Value("${danshop.cfc-backend.url:http://localhost:8080}")
+    private String cfcUrl;
+
+    /**
+     * 验证 JWT token,获取用户信息
+     * @return 用户ID or null
+     */
+    public Long verifyToken(String token) {
+        if (token == null || token.isEmpty()) return null;
+        try {
+            Request request = new Request.Builder()
+                    .url(cfcUrl + "/api/auth/verify")
+                    .header("Authorization", "Bearer " + token)
+                    .get()
+                    .build();
+            try (Response response = CLIENT.newCall(request).execute()) {
+                if (!response.isSuccessful()) return null;
+                String body = response.body().string();
+                JSONObject json = JSONObject.parseObject(body);
+                if (json.getIntValue("code") == 0) {
+                    return json.getJSONObject("data").getLong("userId");
+                }
+            }
+        } catch (Exception ignored) {}
+        return null;
+    }
+
+    /**
+     * 发放能量值给用户
+     */
+    public void grantEnergy(Long userId, Integer amount, String reason) {
+        try {
+            JSONObject body = new JSONObject();
+            body.put("userId", userId);
+            body.put("amount", amount);
+            body.put("reason", reason);
+            Request request = new Request.Builder()
+                    .url(cfcUrl + "/api/energy/grant")
+                    .post(RequestBody.create(JSON_TYPE, body.toJSONString()))
+                    .build();
+            CLIENT.newCall(request).execute();
+        } catch (IOException ignored) {}
+    }
+
+    /**
+     * 查询用户层级关系
+     */
+    public JSONObject getUserRelation(Long userId) {
+        try {
+            Request request = new Request.Builder()
+                    .url(cfcUrl + "/api/user/" + userId + "/relation")
+                    .get()
+                    .build();
+            try (Response response = CLIENT.newCall(request).execute()) {
+                if (!response.isSuccessful()) return null;
+                String body = response.body().string();
+                return JSONObject.parseObject(body);
+            }
+        } catch (Exception ignored) {}
+        return null;
+    }
+}

+ 18 - 0
dan/danshop/src/main/java/com/etotem/danshop/config/MybatisPlusConfig.java

@@ -0,0 +1,18 @@
+package com.etotem.danshop.config;
+
+import com.baomidou.mybatisplus.annotation.DbType;
+import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
+import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class MybatisPlusConfig {
+
+    @Bean
+    public MybatisPlusInterceptor mybatisPlusInterceptor() {
+        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
+        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
+        return interceptor;
+    }
+}

+ 24 - 0
dan/danshop/src/main/java/com/etotem/danshop/config/RedisConfig.java

@@ -0,0 +1,24 @@
+package com.etotem.danshop.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
+import org.springframework.data.redis.serializer.StringRedisSerializer;
+
+@Configuration
+public class RedisConfig {
+
+    @Bean
+    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
+        RedisTemplate<String, Object> template = new RedisTemplate<>();
+        template.setConnectionFactory(factory);
+        template.setKeySerializer(new StringRedisSerializer());
+        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
+        template.setHashKeySerializer(new StringRedisSerializer());
+        template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
+        template.afterPropertiesSet();
+        return template;
+    }
+}

+ 20 - 0
dan/danshop/src/main/java/com/etotem/danshop/config/WebMvcConfig.java

@@ -0,0 +1,20 @@
+package com.etotem.danshop.config;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.CorsRegistry;
+import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+@Configuration
+public class WebMvcConfig implements WebMvcConfigurer {
+
+    @Override
+    public void addCorsMappings(CorsRegistry registry) {
+        registry.addMapping("/**")
+                .allowedOriginPatterns("*")
+                .allowedMethods("GET", "POST", "OPTIONS")
+                .allowedHeaders("*")
+                .allowCredentials(true)
+                .maxAge(3600);
+    }
+}

+ 16 - 0
dan/danshop/src/main/java/com/etotem/danshop/config/WechatPayConfig.java

@@ -0,0 +1,16 @@
+package com.etotem.danshop.config;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import lombok.Data;
+
+@Data
+@ConfigurationProperties(prefix = "danshop.payment.wechat")
+public class WechatPayConfig {
+
+    private String appId;
+    private String mchId;
+    private String apiV3Key;
+    private String privateKeyPath;
+    private String notifyUrl;
+    private String refundNotifyUrl;
+}

+ 20 - 0
dan/danshop/src/main/java/com/etotem/danshop/controller/LogisticsController.java

@@ -0,0 +1,20 @@
+package com.etotem.danshop.controller;
+
+import com.etotem.danshop.common.Result;
+import com.etotem.danshop.service.LogisticsService;
+import org.springframework.web.bind.annotation.*;
+import javax.annotation.Resource;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/api/shop/logistics")
+public class LogisticsController {
+
+    @Resource
+    private LogisticsService logisticsService;
+
+    @PostMapping("/query")
+    public Result<Map<String, Object>> query(@RequestParam Long orderId) {
+        return logisticsService.getLogistics(orderId);
+    }
+}

+ 45 - 0
dan/danshop/src/main/java/com/etotem/danshop/controller/PaymentController.java

@@ -0,0 +1,45 @@
+package com.etotem.danshop.controller;
+
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONObject;
+import com.etotem.danshop.common.Result;
+import com.etotem.danshop.service.OrderService;
+import com.etotem.danshop.service.PaymentService;
+import okhttp3.*;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.web.bind.annotation.*;
+import javax.annotation.Resource;
+import java.io.IOException;
+
+@RestController
+@RequestMapping("/api/shop/payment")
+public class PaymentController {
+
+    private static final MediaType JSON_TYPE = MediaType.parse("application/json; charset=utf-8");
+    private static final OkHttpClient CLIENT = new OkHttpClient();
+
+    @Resource
+    private PaymentService paymentService;
+    @Resource
+    private OrderService orderService;
+
+    @Value("${danshop.payment.wechat.notify-url:}")
+    private String notifyUrl;
+
+    @PostMapping("/createOrder")
+    public Result<JSONObject> createOrder(@RequestParam Long userId,
+                                          @RequestParam String orderNo,
+                                          @RequestParam java.math.BigDecimal amount,
+                                          @RequestParam String openId) {
+        return paymentService.createWxPayOrder(userId, orderNo, amount, openId);
+    }
+
+    @PostMapping("/notify")
+    public String notify(@RequestBody String notifyData) {
+        Result<Void> result = paymentService.handleWxPayNotify(notifyData);
+        if (result.getCode() == 200) {
+            return "SUCCESS";
+        }
+        return "FAIL";
+    }
+}

+ 43 - 0
dan/danshop/src/main/java/com/etotem/danshop/controller/admin/AdminCategoryController.java

@@ -0,0 +1,43 @@
+package com.etotem.danshop.controller.admin;
+
+import com.etotem.danshop.common.Result;
+import com.etotem.danshop.entity.Category;
+import com.etotem.danshop.mapper.CategoryMapper;
+import org.springframework.web.bind.annotation.*;
+import javax.annotation.Resource;
+import java.util.List;
+
+@RestController
+@RequestMapping("/api/admin/shop/category")
+public class AdminCategoryController {
+
+    @Resource
+    private CategoryMapper categoryMapper;
+
+    @PostMapping("/list")
+    public Result<List<Category>> list() {
+        List<Category> list = categoryMapper.selectList(
+            new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<Category>()
+                .orderByAsc(Category::getSortOrder)
+        );
+        return Result.success(list);
+    }
+
+    @PostMapping("/create")
+    public Result<Void> create(@RequestBody Category category) {
+        categoryMapper.insert(category);
+        return Result.success(null);
+    }
+
+    @PostMapping("/update")
+    public Result<Void> update(@RequestBody Category category) {
+        categoryMapper.updateById(category);
+        return Result.success(null);
+    }
+
+    @PostMapping("/delete")
+    public Result<Void> delete(@RequestParam Long id) {
+        categoryMapper.deleteById(id);
+        return Result.success(null);
+    }
+}

+ 33 - 0
dan/danshop/src/main/java/com/etotem/danshop/controller/admin/AdminLogisticsController.java

@@ -0,0 +1,33 @@
+package com.etotem.danshop.controller.admin;
+
+import com.etotem.danshop.common.Result;
+import com.etotem.danshop.entity.Logistics;
+import com.etotem.danshop.mapper.LogisticsMapper;
+import org.springframework.web.bind.annotation.*;
+import javax.annotation.Resource;
+import java.util.List;
+
+@RestController
+@RequestMapping("/api/admin/shop/logistics")
+public class AdminLogisticsController {
+
+    @Resource
+    private LogisticsMapper logisticsMapper;
+
+    @PostMapping("/list")
+    public Result<List<Logistics>> list() {
+        return Result.success(logisticsMapper.selectList(
+            new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<Logistics>()
+                .orderByDesc(Logistics::getCreatedAt)
+        ));
+    }
+
+    @PostMapping("/update")
+    public Result<Void> update(@RequestParam Long id, @RequestParam String status) {
+        Logistics l = logisticsMapper.selectById(id);
+        if (l == null) return Result.error("物流不存在");
+        l.setStatus(status);
+        logisticsMapper.updateById(l);
+        return Result.success(null);
+    }
+}

+ 47 - 0
dan/danshop/src/main/java/com/etotem/danshop/controller/admin/AdminOrderController.java

@@ -0,0 +1,47 @@
+package com.etotem.danshop.controller.admin;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.etotem.danshop.common.Result;
+import com.etotem.danshop.entity.Order;
+import com.etotem.danshop.entity.ProfitLog;
+import com.etotem.danshop.mapper.OrderMapper;
+import com.etotem.danshop.mapper.ProfitLogMapper;
+import com.etotem.danshop.service.ProfitService;
+import org.springframework.web.bind.annotation.*;
+import javax.annotation.Resource;
+import java.util.List;
+
+@RestController
+@RequestMapping("/api/admin/shop/order")
+public class AdminOrderController {
+
+    @Resource
+    private OrderMapper orderMapper;
+    @Resource
+    private ProfitLogMapper profitLogMapper;
+    @Resource
+    private ProfitService profitService;
+
+    @PostMapping("/list")
+    public Result<Page<Order>> list(@RequestParam(required = false) Integer status,
+                                    @RequestParam(defaultValue = "1") Integer page,
+                                    @RequestParam(defaultValue = "20") Integer pageSize) {
+        com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<Order> q =
+            new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<>();
+        if (status != null) q.eq(Order::getStatusInt, status);
+        q.orderByDesc(Order::getCreatedAt);
+        Page<Order> p = orderMapper.selectPage(new Page<>(page, pageSize), q);
+        return Result.success(p);
+    }
+
+    @PostMapping("/profitLogs")
+    public Result<List<ProfitLog>> profitLogs(@RequestParam(required = false) Long vendorId) {
+        if (vendorId != null) {
+            return profitService.listByVendor(vendorId);
+        }
+        return Result.success(profitLogMapper.selectList(
+            new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<ProfitLog>()
+                .orderByDesc(ProfitLog::getCreatedAt)
+        ));
+    }
+}

+ 54 - 0
dan/danshop/src/main/java/com/etotem/danshop/controller/admin/AdminProductController.java

@@ -0,0 +1,54 @@
+package com.etotem.danshop.controller.admin;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.etotem.danshop.common.Result;
+import com.etotem.danshop.entity.Product;
+import com.etotem.danshop.mapper.ProductMapper;
+import org.springframework.web.bind.annotation.*;
+import javax.annotation.Resource;
+
+@RestController
+@RequestMapping("/api/admin/shop/product")
+public class AdminProductController {
+
+    @Resource
+    private ProductMapper productMapper;
+
+    @PostMapping("/list")
+    public Result<Page<Product>> list(@RequestParam(required = false) String status,
+                                      @RequestParam(defaultValue = "1") Integer page,
+                                      @RequestParam(defaultValue = "20") Integer pageSize) {
+        com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<Product> q =
+            new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<>();
+        if (status != null && !status.isEmpty()) q.eq(Product::getStatus, status);
+        q.orderByDesc(Product::getCreatedAt);
+        Page<Product> p = productMapper.selectPage(new Page<>(page, pageSize), q);
+        return Result.success(p);
+    }
+
+    @PostMapping("/review")
+    public Result<Void> review(@RequestParam Long id,
+                               @RequestParam String action,
+                               @RequestParam(required = false) String reason) {
+        Product p = productMapper.selectById(id);
+        if (p == null) return Result.error("商品不存在");
+        if ("approve".equals(action)) {
+            p.setStatus("1"); // 上架
+        } else if ("reject".equals(action)) {
+            p.setStatus("2"); // 下架
+            p.setRejectReason(reason);
+        }
+        productMapper.updateById(p);
+        return Result.success(null);
+    }
+
+    @PostMapping("/setProfitRule")
+    public Result<Void> setProfitRule(@RequestParam Long id,
+                                      @RequestParam java.math.BigDecimal profitRate) {
+        Product p = productMapper.selectById(id);
+        if (p == null) return Result.error("商品不存在");
+        p.setProfitRate(profitRate);
+        productMapper.updateById(p);
+        return Result.success(null);
+    }
+}

+ 53 - 0
dan/danshop/src/main/java/com/etotem/danshop/controller/admin/AdminProfitRuleController.java

@@ -0,0 +1,53 @@
+package com.etotem.danshop.controller.admin;
+
+import com.etotem.danshop.common.Result;
+import com.etotem.danshop.entity.ProfitRule;
+import com.etotem.danshop.mapper.ProfitRuleMapper;
+import org.springframework.web.bind.annotation.*;
+import javax.annotation.Resource;
+import java.math.BigDecimal;
+import java.util.List;
+
+@RestController
+@RequestMapping("/api/admin/shop/profitRule")
+public class AdminProfitRuleController {
+
+    @Resource
+    private ProfitRuleMapper profitRuleMapper;
+
+    @PostMapping("/list")
+    public Result<List<ProfitRule>> list() {
+        return Result.success(profitRuleMapper.selectList(
+            new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<ProfitRule>()
+                .orderByAsc(ProfitRule::getRuleType)
+        ));
+    }
+
+    @PostMapping("/create")
+    public Result<Void> create(@RequestBody ProfitRule rule) {
+        rule.setIsActive(1);
+        profitRuleMapper.insert(rule);
+        return Result.success(null);
+    }
+
+    @PostMapping("/update")
+    public Result<Void> update(@RequestBody ProfitRule rule) {
+        profitRuleMapper.updateById(rule);
+        return Result.success(null);
+    }
+
+    @PostMapping("/delete")
+    public Result<Void> delete(@RequestParam Long id) {
+        profitRuleMapper.deleteById(id);
+        return Result.success(null);
+    }
+
+    @PostMapping("/toggle")
+    public Result<Void> toggle(@RequestParam Long id, @RequestParam Integer isActive) {
+        ProfitRule r = profitRuleMapper.selectById(id);
+        if (r == null) return Result.error("规则不存在");
+        r.setIsActive(isActive);
+        profitRuleMapper.updateById(r);
+        return Result.success(null);
+    }
+}

+ 46 - 0
dan/danshop/src/main/java/com/etotem/danshop/controller/buyer/AddressController.java

@@ -0,0 +1,46 @@
+package com.etotem.danshop.controller.buyer;
+
+import com.etotem.danshop.common.Result;
+import com.etotem.danshop.dto.AddressDTO;
+import com.etotem.danshop.service.AddressService;
+import org.springframework.web.bind.annotation.*;
+import javax.annotation.Resource;
+import java.util.List;
+
+@RestController
+@RequestMapping("/api/shop/address")
+public class AddressController {
+
+    @Resource
+    private AddressService addressService;
+
+    @PostMapping("/list")
+    public Result<List<AddressDTO>> list(@RequestAttribute Long userId) {
+        return addressService.listByUser(userId);
+    }
+
+    @PostMapping("/get")
+    public Result<AddressDTO> get(@RequestParam Long id) {
+        return addressService.getById(id);
+    }
+
+    @PostMapping("/create")
+    public Result<Void> create(@RequestAttribute Long userId, @RequestBody AddressDTO dto) {
+        return addressService.create(userId, dto);
+    }
+
+    @PostMapping("/update")
+    public Result<Void> update(@RequestParam Long id, @RequestBody AddressDTO dto) {
+        return addressService.update(id, dto);
+    }
+
+    @PostMapping("/delete")
+    public Result<Void> delete(@RequestParam Long id) {
+        return addressService.delete(id);
+    }
+
+    @PostMapping("/setDefault")
+    public Result<Void> setDefault(@RequestAttribute Long userId, @RequestParam Long addressId) {
+        return addressService.setDefault(userId, addressId);
+    }
+}

+ 42 - 0
dan/danshop/src/main/java/com/etotem/danshop/controller/buyer/CartController.java

@@ -0,0 +1,42 @@
+package com.etotem.danshop.controller.buyer;
+
+import com.etotem.danshop.common.Result;
+import com.etotem.danshop.service.CartService;
+import org.springframework.web.bind.annotation.*;
+import javax.annotation.Resource;
+
+@RestController
+@RequestMapping("/api/shop/cart")
+public class CartController {
+
+    @Resource
+    private CartService cartService;
+
+    @PostMapping("/list")
+    public Result<?> list(@RequestAttribute Long userId) {
+        return cartService.getCart(userId);
+    }
+
+    @PostMapping("/add")
+    public Result<?> add(@RequestAttribute Long userId,
+                         @RequestParam Long productId,
+                         @RequestParam(defaultValue = "1") Integer quantity) {
+        return cartService.addItem(userId, productId, quantity);
+    }
+
+    @PostMapping("/remove")
+    public Result<?> remove(@RequestAttribute Long userId,
+                           @RequestParam Long productId) {
+        return cartService.removeItem(userId, productId);
+    }
+
+    @PostMapping("/clear")
+    public Result<?> clear(@RequestAttribute Long userId) {
+        return cartService.clearCart(userId);
+    }
+
+    @PostMapping("/total")
+    public Result<?> total(@RequestAttribute Long userId) {
+        return cartService.calculateTotal(userId);
+    }
+}

+ 24 - 0
dan/danshop/src/main/java/com/etotem/danshop/controller/buyer/CategoryController.java

@@ -0,0 +1,24 @@
+package com.etotem.danshop.controller.buyer;
+
+import com.etotem.danshop.common.Result;
+import com.etotem.danshop.service.CategoryService;
+import org.springframework.web.bind.annotation.*;
+import javax.annotation.Resource;
+
+@RestController
+@RequestMapping("/api/shop/category")
+public class CategoryController {
+
+    @Resource
+    private CategoryService categoryService;
+
+    @PostMapping("/list")
+    public Result<?> list() {
+        return categoryService.listEnabled();
+    }
+
+    @PostMapping("/listAll")
+    public Result<?> listAll() {
+        return categoryService.listAll();
+    }
+}

+ 59 - 0
dan/danshop/src/main/java/com/etotem/danshop/controller/buyer/OrderController.java

@@ -0,0 +1,59 @@
+package com.etotem.danshop.controller.buyer;
+
+import com.etotem.danshop.common.Result;
+import com.etotem.danshop.dto.OrderCreateDTO;
+import com.etotem.danshop.dto.OrderDTO;
+import com.etotem.danshop.service.OrderService;
+import com.etotem.danshop.service.PaymentService;
+import org.springframework.web.bind.annotation.*;
+import javax.annotation.Resource;
+import java.util.List;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/api/shop/order")
+public class OrderController {
+
+    @Resource
+    private OrderService orderService;
+    @Resource
+    private PaymentService paymentService;
+
+    @PostMapping("/create")
+    public Result<OrderDTO> create(@RequestAttribute Long userId, @RequestBody OrderCreateDTO dto) {
+        return orderService.create(userId, dto);
+    }
+
+    @PostMapping("/list")
+    public Result<List<OrderDTO>> list(@RequestAttribute Long userId,
+                                       @RequestParam(required = false) Integer status) {
+        return orderService.listByBuyer(userId, status);
+    }
+
+    @PostMapping("/detail")
+    public Result<OrderDTO> detail(@RequestParam String orderNo) {
+        return orderService.getByOrderNo(orderNo);
+    }
+
+    @PostMapping("/pay")
+    public Result<?> pay(@RequestAttribute Long userId,
+                        @RequestParam String orderNo,
+                        @RequestParam String openId) {
+        // 先查订单金额
+        Result<OrderDTO> orderResult = orderService.getByOrderNo(orderNo);
+        if (orderResult.getData() == null) return Result.error("订单不存在");
+        return paymentService.createWxPayOrder(userId, orderNo,
+            orderResult.getData().getTotalAmount(), openId);
+    }
+
+    @PostMapping("/confirmReceive")
+    public Result<Void> confirmReceive(@RequestAttribute Long userId, @RequestParam Long orderId) {
+        return orderService.confirmReceive(orderId);
+    }
+
+    @PostMapping("/cancel")
+    public Result<Void> cancel(@RequestAttribute Long userId, @RequestParam String orderNo) {
+        // TODO: 实现取消
+        return Result.success(null);
+    }
+}

+ 40 - 0
dan/danshop/src/main/java/com/etotem/danshop/controller/buyer/ProductController.java

@@ -0,0 +1,40 @@
+package com.etotem.danshop.controller.buyer;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.etotem.danshop.common.Result;
+import com.etotem.danshop.entity.Product;
+import com.etotem.danshop.mapper.ProductMapper;
+import com.etotem.danshop.service.ProductService;
+import org.springframework.web.bind.annotation.*;
+import javax.annotation.Resource;
+
+@RestController
+@RequestMapping("/api/shop/product")
+public class ProductController {
+
+    @Resource
+    private ProductService productService;
+    @Resource
+    private ProductMapper productMapper;
+
+    @PostMapping("/list")
+    public Result<?> list(
+            @RequestParam(required = false) Long categoryId,
+            @RequestParam(defaultValue = "1") Integer page,
+            @RequestParam(defaultValue = "20") Integer pageSize) {
+        return productService.listByCategory(categoryId, page, pageSize);
+    }
+
+    @PostMapping("/detail")
+    public Result<?> detail(@RequestParam Long id) {
+        return productService.getDetail(id);
+    }
+
+    @PostMapping("/search")
+    public Result<?> search(
+            @RequestParam(required = false) String keyword,
+            @RequestParam(defaultValue = "1") Integer page,
+            @RequestParam(defaultValue = "20") Integer pageSize) {
+        return productService.search(keyword, page, pageSize);
+    }
+}

+ 44 - 0
dan/danshop/src/main/java/com/etotem/danshop/controller/seller/SellerOrderController.java

@@ -0,0 +1,44 @@
+package com.etotem.danshop.controller.seller;
+
+import com.etotem.danshop.common.Result;
+import com.etotem.danshop.entity.Order;
+import com.etotem.danshop.mapper.OrderMapper;
+import com.etotem.danshop.service.OrderService;
+import com.etotem.danshop.service.LogisticsService;
+import org.springframework.web.bind.annotation.*;
+import javax.annotation.Resource;
+import java.util.List;
+
+@RestController
+@RequestMapping("/api/seller/order")
+public class SellerOrderController {
+
+    @Resource
+    private OrderMapper orderMapper;
+    @Resource
+    private OrderService orderService;
+    @Resource
+    private LogisticsService logisticsService;
+
+    @PostMapping("/list")
+    public Result<List<Order>> list(@RequestAttribute Long sellerId,
+                                     @RequestParam(required = false) Integer status) {
+        com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<Order> q =
+            new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<>();
+        if (status != null) q.eq(Order::getStatusInt, status);
+        q.orderByDesc(Order::getCreatedAt);
+        return Result.success(orderMapper.selectList(q));
+    }
+
+    @PostMapping("/ship")
+    public Result<Void> ship(@RequestParam Long orderId,
+                             @RequestParam String company,
+                             @RequestParam String companyName,
+                             @RequestParam String trackingNo) {
+        Result<Void> r = orderService.ship(orderId, company, trackingNo);
+        if (r.getCode() == 200) {
+            logisticsService.createLogistics(orderId, company, companyName, trackingNo);
+        }
+        return r;
+    }
+}

+ 51 - 0
dan/danshop/src/main/java/com/etotem/danshop/controller/seller/SellerProductController.java

@@ -0,0 +1,51 @@
+package com.etotem.danshop.controller.seller;
+
+import com.etotem.danshop.common.Result;
+import com.etotem.danshop.entity.Product;
+import com.etotem.danshop.mapper.ProductMapper;
+import org.springframework.web.bind.annotation.*;
+import javax.annotation.Resource;
+import java.math.BigDecimal;
+import java.util.List;
+
+@RestController
+@RequestMapping("/api/seller/product")
+public class SellerProductController {
+
+    @Resource
+    private ProductMapper productMapper;
+
+    @PostMapping("/list")
+    public Result<List<Product>> list(@RequestAttribute Long sellerId,
+                                      @RequestParam(defaultValue = "1") Integer page,
+                                      @RequestParam(defaultValue = "20") Integer pageSize) {
+        com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<Product> q =
+            new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<>();
+        q.eq(Product::getVendorId, sellerId)
+         .orderByDesc(com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper::getCreatedAt);
+        com.baomidou.mybatisplus.extension.plugins.pagination.Page<Product> p =
+            productMapper.selectPage(new com.baomidou.mybatisplus.extension.plugins.pagination.Page<>(page, pageSize), q);
+        return Result.success(p.getRecords());
+    }
+
+    @PostMapping("/create")
+    public Result<Void> create(@RequestAttribute Long sellerId, @RequestBody Product product) {
+        product.setVendorId(sellerId);
+        product.setStatus("pending");
+        productMapper.insert(product);
+        return Result.success(null);
+    }
+
+    @PostMapping("/update")
+    public Result<Void> update(@RequestParam Long id, @RequestBody Product product) {
+        product.setId(id);
+        productMapper.updateById(product);
+        return Result.success(null);
+    }
+
+    @PostMapping("/delete")
+    public Result<Void> delete(@RequestParam Long id) {
+        productMapper.deleteById(id);
+        return Result.success(null);
+    }
+}

+ 20 - 0
dan/danshop/src/main/java/com/etotem/danshop/dto/AddressDTO.java

@@ -0,0 +1,20 @@
+package com.etotem.danshop.dto;
+
+import lombok.Data;
+import java.io.Serializable;
+import java.math.BigDecimal;
+
+@Data
+public class AddressDTO implements Serializable {
+    private Long id;
+    private Long userId;
+    private String receiverName;
+    private String phone;
+    private String province;
+    private String city;
+    private String district;
+    private String street;
+    private String detail;
+    private Integer isDefault;
+    private String fullAddress;
+}

+ 17 - 0
dan/danshop/src/main/java/com/etotem/danshop/dto/CartItemDTO.java

@@ -0,0 +1,17 @@
+package com.etotem.danshop.dto;
+
+import lombok.Data;
+import java.io.Serializable;
+import java.math.BigDecimal;
+
+@Data
+public class CartItemDTO implements Serializable {
+    private Long id;
+    private Long productId;
+    private String productName;
+    private String coverImage;
+    private BigDecimal price;
+    private Integer quantity;
+    private Long skuId;
+    private String skuName;
+}

+ 13 - 0
dan/danshop/src/main/java/com/etotem/danshop/dto/CategoryDTO.java

@@ -0,0 +1,13 @@
+package com.etotem.danshop.dto;
+
+import lombok.Data;
+import java.io.Serializable;
+
+@Data
+public class CategoryDTO implements Serializable {
+    private Long id;
+    private String name;
+    private String icon;
+    private Integer sort;
+    private Integer productCount;
+}

+ 16 - 0
dan/danshop/src/main/java/com/etotem/danshop/dto/LogisticsDTO.java

@@ -0,0 +1,16 @@
+package com.etotem.danshop.dto;
+
+import lombok.Data;
+import java.io.Serializable;
+
+@Data
+public class LogisticsDTO implements Serializable {
+    private Long id;
+    private Long orderId;
+    private String company;
+    private String trackingNo;
+    private Integer status;
+    private String statusName;
+    private String currentInfo;
+    private String updatedAt;
+}

+ 20 - 0
dan/danshop/src/main/java/com/etotem/danshop/dto/OrderCreateDTO.java

@@ -0,0 +1,20 @@
+package com.etotem.danshop.dto;
+
+import lombok.Data;
+import java.io.Serializable;
+import java.util.List;
+
+@Data
+public class OrderCreateDTO implements Serializable {
+    private List<CartItem> items;
+    private Long addressId;
+    private String remark;
+    private Integer paymentMethod;
+
+    @Data
+    public static class CartItem {
+        private Long productId;
+        private Long skuId;
+        private Integer quantity;
+    }
+}

+ 26 - 0
dan/danshop/src/main/java/com/etotem/danshop/dto/OrderDTO.java

@@ -0,0 +1,26 @@
+package com.etotem.danshop.dto;
+
+import lombok.Data;
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.util.List;
+
+@Data
+public class OrderDTO implements Serializable {
+    private Long id;
+    private String orderNo;
+    private Long buyerId;
+    private BigDecimal totalAmount;
+    private BigDecimal freight;
+    private BigDecimal profitAmount;
+    private Integer status;
+    private String statusName;
+    private Integer paymentMethod;
+    private String paymentTime;
+    private String remark;
+    private String receiverName;
+    private String receiverPhone;
+    private String receiverAddress;
+    private List<OrderItemDTO> items;
+    private String createdAt;
+}

+ 17 - 0
dan/danshop/src/main/java/com/etotem/danshop/dto/OrderItemDTO.java

@@ -0,0 +1,17 @@
+package com.etotem.danshop.dto;
+
+import lombok.Data;
+import java.io.Serializable;
+import java.math.BigDecimal;
+
+@Data
+public class OrderItemDTO implements Serializable {
+    private Long id;
+    private Long orderId;
+    private Long productId;
+    private String productName;
+    private String coverImage;
+    private BigDecimal price;
+    private Integer quantity;
+    private BigDecimal subtotal;
+}

+ 14 - 0
dan/danshop/src/main/java/com/etotem/danshop/dto/PayNotifyDTO.java

@@ -0,0 +1,14 @@
+package com.etotem.danshop.dto;
+
+import lombok.Data;
+import java.io.Serializable;
+import java.math.BigDecimal;
+
+@Data
+public class PayNotifyDTO implements Serializable {
+    private String orderNo;
+    private String transactionId;
+    private String tradeState;
+    private BigDecimal totalFee;
+    private String successTime;
+}

+ 26 - 0
dan/danshop/src/main/java/com/etotem/danshop/dto/ProductDTO.java

@@ -0,0 +1,26 @@
+package com.etotem.danshop.dto;
+
+import lombok.Data;
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.util.List;
+
+@Data
+public class ProductDTO implements Serializable {
+    private Long id;
+    private Long categoryId;
+    private String name;
+    private String subtitle;
+    private String coverImage;
+    private List<String> images;
+    private BigDecimal price;
+    private BigDecimal originalPrice;
+    private Integer stock;
+    private String unit;
+    private String description;
+    private String status;
+    private Long sellerId;
+    private String sellerName;
+    private Boolean hasProfit;
+    private Integer salesCount;
+}

+ 15 - 0
dan/danshop/src/main/java/com/etotem/danshop/dto/ProfitRuleDTO.java

@@ -0,0 +1,15 @@
+package com.etotem.danshop.dto;
+
+import lombok.Data;
+import java.io.Serializable;
+import java.math.BigDecimal;
+
+@Data
+public class ProfitRuleDTO implements Serializable {
+    private Long id;
+    private Integer level;
+    private String levelName;
+    private BigDecimal ratio;
+    private String description;
+    private Boolean enabled;
+}

+ 40 - 0
dan/danshop/src/main/java/com/etotem/danshop/entity/Address.java

@@ -0,0 +1,40 @@
+package com.etotem.danshop.entity;
+
+import com.baomidou.mybatisplus.annotation.*;
+import lombok.Data;
+import java.io.Serializable;
+import java.util.Date;
+
+@Data
+@TableName("danshop_addresses")
+public class Address implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long userId;
+
+    private String receiverName;
+
+    private String phone;
+
+    private String province;
+
+    private String city;
+
+    private String district;
+
+    private String street;
+
+    private Integer isDefault;
+
+    @TableField(fill = FieldFill.INSERT)
+    private Date createdAt;
+
+    @TableField(fill = FieldFill.INSERT_UPDATE)
+    private Date updatedAt;
+
+    // === 非数据库字段 ===
+    @TableField(exist = false)
+    private String fullAddress;  // 省份+城市+区县+街道
+}

+ 41 - 0
dan/danshop/src/main/java/com/etotem/danshop/entity/Category.java

@@ -0,0 +1,41 @@
+package com.etotem.danshop.entity;
+
+import com.baomidou.mybatisplus.annotation.*;
+import lombok.Data;
+import java.io.Serializable;
+import java.util.Date;
+import java.util.List;
+
+@Data
+@TableName("danshop_categories")
+public class Category implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long parentId;
+
+    private String name;
+
+    private String image;
+
+    private Integer sortOrder;
+
+    private Long energyDimensionId;
+
+    private Integer status;
+
+    @TableField(fill = FieldFill.INSERT)
+    private Date createdAt;
+
+    @TableField(fill = FieldFill.INSERT_UPDATE)
+    private Date updatedAt;
+
+    // 非数据库字段:子分类列表(用于树结构)
+    @TableField(exist = false)
+    private List<Category> children;
+
+    // 非数据库字段:分类名称(用于关联查询显示)
+    @TableField(exist = false)
+    private String dimensionName;
+}

+ 21 - 0
dan/danshop/src/main/java/com/etotem/danshop/entity/Favorite.java

@@ -0,0 +1,21 @@
+package com.etotem.danshop.entity;
+
+import com.baomidou.mybatisplus.annotation.*;
+import lombok.Data;
+import java.io.Serializable;
+import java.util.Date;
+
+@Data
+@TableName("danshop_favorites")
+public class Favorite implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long userId;
+
+    private Long productId;
+
+    @TableField(fill = FieldFill.INSERT)
+    private Date createdAt;
+}

+ 50 - 0
dan/danshop/src/main/java/com/etotem/danshop/entity/Logistics.java

@@ -0,0 +1,50 @@
+package com.etotem.danshop.entity;
+
+import com.baomidou.mybatisplus.annotation.*;
+import lombok.Data;
+import java.io.Serializable;
+import java.util.Date;
+
+@Data
+@TableName("danshop_logistics")
+public class Logistics implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long orderId;
+
+    private Long orderItemId;
+
+    private String logisticsNo;
+
+    private String logisticsCompany;
+
+    private String logisticsName;
+
+    private String senderName;
+
+    private String senderPhone;
+
+    private String senderAddress;
+
+    private String receiverName;
+
+    private String receiverPhone;
+
+    private String receiverAddress;
+
+    private String status;
+
+    private String trackingData;
+
+    private Date shippedAt;
+
+    private Date signedAt;
+
+    @TableField(fill = FieldFill.INSERT)
+    private Date createdAt;
+
+    @TableField(fill = FieldFill.INSERT_UPDATE)
+    private Date updatedAt;
+}

+ 72 - 0
dan/danshop/src/main/java/com/etotem/danshop/entity/Order.java

@@ -0,0 +1,72 @@
+package com.etotem.danshop.entity;
+
+import com.baomidou.mybatisplus.annotation.*;
+import lombok.Data;
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.util.Date;
+import java.util.List;
+
+@Data
+@TableName("danshop_orders")
+public class Order implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private String orderNo;
+
+    private Long buyerId;
+
+    private Long familyId;
+
+    private BigDecimal totalAmount;
+
+    private BigDecimal discountAmount;
+
+    private BigDecimal actualAmount;
+
+    private BigDecimal freight;
+
+    private String status;
+
+    private Integer statusInt;
+
+    private String paymentMethod;
+
+    private String transactionId;
+
+    private Date paidAt;
+
+    private Date shippedAt;
+
+    private Date completedAt;
+
+    private String remark;
+
+    private String cancelReason;
+
+    private String receiverName;
+
+    private String receiverPhone;
+
+    private String receiverAddress;
+
+    private BigDecimal profitAmount;
+
+    @TableField(fill = FieldFill.INSERT)
+    private Date createdAt;
+
+    @TableField(fill = FieldFill.INSERT_UPDATE)
+    private Date updatedAt;
+
+    // === 非数据库字段 ===
+    @TableField(exist = false)
+    private String buyerName;
+
+    @TableField(exist = false)
+    private List<OrderItem> items;
+
+    @TableField(exist = false)
+    private Logistics logistics;
+}

+ 40 - 0
dan/danshop/src/main/java/com/etotem/danshop/entity/OrderItem.java

@@ -0,0 +1,40 @@
+package com.etotem.danshop.entity;
+
+import com.baomidou.mybatisplus.annotation.*;
+import lombok.Data;
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.util.Date;
+
+@Data
+@TableName("danshop_order_items")
+public class OrderItem implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long orderId;
+
+    private Long productId;
+
+    private String productName;
+
+    private String coverImage;
+
+    private BigDecimal price;
+
+    private BigDecimal unitPrice;
+
+    private Integer quantity;
+
+    private BigDecimal subtotal;
+
+    private String productType;
+
+    private Long vendorId;
+
+    private String vendorName;
+
+    @TableField(fill = FieldFill.INSERT)
+    private Date createdAt;
+}

+ 27 - 0
dan/danshop/src/main/java/com/etotem/danshop/entity/OrderLog.java

@@ -0,0 +1,27 @@
+package com.etotem.danshop.entity;
+
+import com.baomidou.mybatisplus.annotation.*;
+import lombok.Data;
+import java.io.Serializable;
+import java.util.Date;
+
+@Data
+@TableName("danshop_order_logs")
+public class OrderLog implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long orderId;
+
+    private String action;
+
+    private Long operatorId;
+
+    private String operatorType;
+
+    private String detail;
+
+    @TableField(fill = FieldFill.INSERT)
+    private Date createdAt;
+}

+ 72 - 0
dan/danshop/src/main/java/com/etotem/danshop/entity/Product.java

@@ -0,0 +1,72 @@
+package com.etotem.danshop.entity;
+
+import com.baomidou.mybatisplus.annotation.*;
+import lombok.Data;
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.util.Date;
+
+@Data
+@TableName("danshop_products")
+public class Product implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long categoryId;
+
+    private String name;
+
+    private String description;
+
+    private String coverImage;
+
+    private String images;
+
+    private BigDecimal price;
+
+    private BigDecimal memberPrice;
+
+    private BigDecimal costPrice;
+
+    private Integer stock;
+
+    private Integer salesCount;
+
+    private String productType;
+
+    private Long vendorId;
+
+    private String vendorName;
+
+    private String status;
+
+    private String rejectReason;
+
+    private BigDecimal profitRate;
+
+    private String externalSource;
+
+    private String externalId;
+
+    private String externalData;
+
+    @TableField(fill = FieldFill.INSERT)
+    private Date createdAt;
+
+    @TableField(fill = FieldFill.INSERT_UPDATE)
+    private Date updatedAt;
+
+    // === 非数据库字段 ===
+    @TableField(exist = false)
+    private String categoryName;
+
+    @TableField(exist = false)
+    private Long energyDimensionId;
+
+    @TableField(exist = false)
+    private String dimensionName;
+
+    @TableField(exist = false)
+    private Boolean canBuy;  // 是否可购买(库存>0且已上架)
+}

+ 38 - 0
dan/danshop/src/main/java/com/etotem/danshop/entity/ProfitLog.java

@@ -0,0 +1,38 @@
+package com.etotem.danshop.entity;
+
+import com.baomidou.mybatisplus.annotation.*;
+import lombok.Data;
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.util.Date;
+
+@Data
+@TableName("danshop_profit_logs")
+public class ProfitLog implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long orderId;
+
+    private String orderNo;
+
+    private BigDecimal orderAmount;
+
+    private BigDecimal costAmount;
+
+    private BigDecimal profitAmount;
+
+    private BigDecimal platformRetained;
+
+    private BigDecimal vendorIncome;
+
+    private BigDecimal commissionL1;
+
+    private BigDecimal commissionL2;
+
+    private Long ruleId;
+
+    @TableField(fill = FieldFill.INSERT)
+    private Date createdAt;
+}

+ 37 - 0
dan/danshop/src/main/java/com/etotem/danshop/entity/ProfitRule.java

@@ -0,0 +1,37 @@
+package com.etotem.danshop.entity;
+
+import com.baomidou.mybatisplus.annotation.*;
+import lombok.Data;
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.util.Date;
+
+@Data
+@TableName("danshop_profit_rules")
+public class ProfitRule implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private String ruleName;
+
+    private String ruleType;
+
+    private Long targetId;
+
+    private BigDecimal platformProfitRate;
+
+    private BigDecimal referrerL1Rate;
+
+    private BigDecimal referrerL2Rate;
+
+    private BigDecimal platformServiceRate;
+
+    private Integer isActive;
+
+    @TableField(fill = FieldFill.INSERT)
+    private Date createdAt;
+
+    @TableField(fill = FieldFill.INSERT_UPDATE)
+    private Date updatedAt;
+}

+ 9 - 0
dan/danshop/src/main/java/com/etotem/danshop/mapper/AddressMapper.java

@@ -0,0 +1,9 @@
+package com.etotem.danshop.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.danshop.entity.Address;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface AddressMapper extends BaseMapper<Address> {
+}

+ 9 - 0
dan/danshop/src/main/java/com/etotem/danshop/mapper/CategoryMapper.java

@@ -0,0 +1,9 @@
+package com.etotem.danshop.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.danshop.entity.Category;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface CategoryMapper extends BaseMapper<Category> {
+}

+ 9 - 0
dan/danshop/src/main/java/com/etotem/danshop/mapper/FavoriteMapper.java

@@ -0,0 +1,9 @@
+package com.etotem.danshop.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.danshop.entity.Favorite;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface FavoriteMapper extends BaseMapper<Favorite> {
+}

+ 9 - 0
dan/danshop/src/main/java/com/etotem/danshop/mapper/LogisticsMapper.java

@@ -0,0 +1,9 @@
+package com.etotem.danshop.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.danshop.entity.Logistics;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface LogisticsMapper extends BaseMapper<Logistics> {
+}

+ 9 - 0
dan/danshop/src/main/java/com/etotem/danshop/mapper/OrderItemMapper.java

@@ -0,0 +1,9 @@
+package com.etotem.danshop.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.danshop.entity.OrderItem;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface OrderItemMapper extends BaseMapper<OrderItem> {
+}

+ 9 - 0
dan/danshop/src/main/java/com/etotem/danshop/mapper/OrderLogMapper.java

@@ -0,0 +1,9 @@
+package com.etotem.danshop.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.danshop.entity.OrderLog;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface OrderLogMapper extends BaseMapper<OrderLog> {
+}

+ 9 - 0
dan/danshop/src/main/java/com/etotem/danshop/mapper/OrderMapper.java

@@ -0,0 +1,9 @@
+package com.etotem.danshop.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.danshop.entity.Order;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface OrderMapper extends BaseMapper<Order> {
+}

この差分においてかなりの量のファイルが変更されているため、一部のファイルを表示していません