Преглед изворни кода

Merge remote-tracking branch 'origin/cfclub' into cfclub

asus пре 2 месеци
родитељ
комит
f9fcbeb246

+ 8 - 4
cfc-backend/src/main/java/com/etotem/cfc/controller/MarketController.java

@@ -8,6 +8,7 @@ import com.etotem.cfc.entity.TaskPlanInstance;
 import com.etotem.cfc.entity.TaskPlanItem;
 import com.etotem.cfc.entity.TaskTemplatePackage;
 import com.etotem.cfc.entity.TaskTemplateItem;
+import com.etotem.cfc.service.MembershipService;
 import com.etotem.cfc.service.TaskPlanService;
 import com.etotem.cfc.service.TaskTemplatePackageService;
 import io.swagger.v3.oas.annotations.Operation;
@@ -29,6 +30,9 @@ public class MarketController {
     @Resource
     private TaskPlanService planService;
 
+    @Resource
+    private MembershipService membershipService;
+
     /**
      * 获取公开任务模板列表 (市场)
      */
@@ -81,9 +85,8 @@ public class MarketController {
     public Result<TaskPlanInstance> purchasePackage(
             @PathVariable Long id,
             @RequestBody ApplyPackageDTO dto,
-            @RequestAttribute("userId") Long userId,
-            @RequestAttribute("familyId") Long familyId) {
-        
+            @RequestAttribute("userId") Long userId) {
+        Long familyId = membershipService.getUserFamilyId(userId);
         dto.setPackageId(id);
         TaskPlanInstance instance = planService.applyPackage(dto, familyId);
         return Result.success(instance);
@@ -94,7 +97,8 @@ public class MarketController {
      */
     @Operation(summary = "获取我的任务计划")
     @PostMapping("/plans")
-    public Result<List<TaskPlanInstance>> getMyPlans(@RequestAttribute("familyId") Long familyId) {
+    public Result<List<TaskPlanInstance>> getMyPlans(@RequestAttribute("userId") Long userId) {
+        Long familyId = membershipService.getUserFamilyId(userId);
         List<TaskPlanInstance> plans = planService.getFamilyPlans(familyId);
         return Result.success(plans);
     }

+ 5 - 1
cfc-backend/src/main/java/com/etotem/cfc/controller/PackagePaymentController.java

@@ -4,6 +4,7 @@ import javax.annotation.Resource;
 import com.etotem.cfc.common.Result;
 import com.etotem.cfc.entity.PackageOrder;
 import com.etotem.cfc.entity.TaskPlanInstance;
+import com.etotem.cfc.service.MembershipService;
 import com.etotem.cfc.service.PackagePaymentService;
 import com.etotem.cfc.service.TaskPlanService;
 import io.swagger.v3.oas.annotations.Operation;
@@ -23,6 +24,9 @@ public class PackagePaymentController {
     @Resource
     private TaskPlanService planService;
 
+    @Resource
+    private MembershipService membershipService;
+
     /**
      * 创建支付订单
      */
@@ -30,8 +34,8 @@ public class PackagePaymentController {
     @PostMapping("/create")
     public Result<PackageOrder> createOrder(
             @RequestAttribute("userId") Long userId,
-            @RequestAttribute("familyId") Long familyId,
             @RequestBody Map<String, Object> params) {
+        Long familyId = membershipService.getUserFamilyId(userId);
         Long packageId = Long.valueOf(params.get("packageId").toString());
         String payMethod = params.get("payMethod") != null ? params.get("payMethod").toString() : "wechat";
         Long couponId = params.get("couponId") != null ? Long.valueOf(params.get("couponId").toString()) : null;

+ 9 - 4
cfc-backend/src/main/java/com/etotem/cfc/controller/family/BindController.java

@@ -5,6 +5,7 @@ import com.etotem.cfc.common.Result;
 import com.etotem.cfc.dto.BindGuideDTO;
 import com.etotem.cfc.entity.GuideFamily;
 import com.etotem.cfc.service.GuideFamilyService;
+import com.etotem.cfc.service.MembershipService;
 import com.etotem.cfc.service.TeacherMessageService;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.tags.Tag;
@@ -26,15 +27,18 @@ public class BindController {
     @Resource
     private TeacherMessageService teacherMessageService;
 
+    @Resource
+    private MembershipService membershipService;
+
     /**
      * 家长确认绑定指导师
      */
     @Operation(summary = "确认绑定指导师")
     @PostMapping("/accept")
     public Result<Boolean> acceptBind(@RequestAttribute("userId") Long userId,
-                                        @RequestAttribute("familyId") Long familyId,
                                         @RequestBody BindGuideDTO dto) {
         try {
+            Long familyId = membershipService.getUserFamilyId(userId);
             boolean success = guideFamilyService.confirmBind(dto.getGuideId(), familyId,
                 dto.getServiceType(), dto.getServicePrice());
             return Result.success(success);
@@ -48,7 +52,8 @@ public class BindController {
      */
     @Operation(summary = "获取绑定的指导师列表")
     @PostMapping("/guides")
-    public Result<List<Map<String, Object>>> getBoundGuides(@RequestAttribute("familyId") Long familyId) {
+    public Result<List<Map<String, Object>>> getBoundGuides(@RequestAttribute("userId") Long userId) {
+        Long familyId = membershipService.getUserFamilyId(userId);
         List<GuideFamily> bindings = guideFamilyService.getGuidesByFamily(familyId);
         
         List<Map<String, Object>> result = bindings.stream().map(binding -> {
@@ -71,8 +76,8 @@ public class BindController {
     @Operation(summary = "解除绑定规划师")
     @PostMapping("/unbind")
     public Result<Boolean> unbind(@RequestAttribute("userId") Long userId,
-                                   @RequestAttribute("familyId") Long familyId,
-                                   @RequestBody Map<String, Object> params) {
+                                    @RequestBody Map<String, Object> params) {
+        Long familyId = membershipService.getUserFamilyId(userId);
         Long id = Long.valueOf(params.get("id").toString());
         try {
             GuideFamily gf = guideFamilyService.unbindAndGet(id, familyId);

+ 10 - 3
cfc-backend/src/main/java/com/etotem/cfc/controller/mind/MindFortuneController.java

@@ -4,6 +4,7 @@ import com.etotem.cfc.common.Result;
 import com.etotem.cfc.entity.FamilyFortune;
 import com.etotem.cfc.entity.FamilyFortuneReport;
 import com.etotem.cfc.service.FortuneService;
+import com.etotem.cfc.service.MembershipService;
 import com.etotem.cfc.service.PdfReportService;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestAttribute;
@@ -24,12 +25,16 @@ public class MindFortuneController {
     
     @Resource
     private PdfReportService pdfReportService;
+
+    @Resource
+    private MembershipService membershipService;
     
     /**
      * 获取今日家庭运势
      */
     @PostMapping("/fortune")
-    public Result<Map<String, Object>> getTodayFortune(@RequestAttribute("familyId") Long familyId) {
+    public Result<Map<String, Object>> getTodayFortune(@RequestAttribute("userId") Long userId) {
+        Long familyId = membershipService.getUserFamilyId(userId);
         FamilyFortune fortune = fortuneService.getFamilyFortune(familyId, LocalDate.now());
         Map<String, Object> result = batchBuildFortuneResponse(fortune);
         return Result.success(result);
@@ -39,7 +44,8 @@ public class MindFortuneController {
      * 手动刷新运势(仅供测试)
      */
     @PostMapping("/fortune/refresh")
-    public Result<FamilyFortune> refreshFortune(@RequestAttribute("familyId") Long familyId) {
+    public Result<FamilyFortune> refreshFortune(@RequestAttribute("userId") Long userId) {
+        Long familyId = membershipService.getUserFamilyId(userId);
         FamilyFortune fortune = fortuneService.getFamilyFortune(familyId, LocalDate.now());
         fortune.setLuckyDirection(fortuneService.calculateLuckyDirection(familyId, LocalDate.now()));
         fortuneService.saveFamilyFortune(fortune);
@@ -53,8 +59,9 @@ public class MindFortuneController {
      */
     @PostMapping("/fortune/export")
     public Result<Map<String, Object>> exportFortuneReport(
-            @RequestAttribute("familyId") Long familyId,
+            @RequestAttribute("userId") Long userId,
             @RequestBody ExportReportRequest request) {
+        Long familyId = membershipService.getUserFamilyId(userId);
         try {
             // 文件大小校验
             if (request.imageBase64 == null || request.imageBase64.length() > 4 * 1024 * 1024) {

+ 14 - 5
cfc-backend/src/test/java/com/etotem/cfc/integration/PlannerBindInviteFlowTest.java

@@ -7,6 +7,7 @@ import com.etotem.cfc.dto.BindGuideDTO;
 import com.etotem.cfc.entity.GuideFamily;
 import com.etotem.cfc.service.BindInviteService;
 import com.etotem.cfc.service.GuideFamilyService;
+import com.etotem.cfc.service.MembershipService;
 import com.etotem.cfc.service.TeacherMessageService;
 import com.etotem.cfc.mapper.GuideFamilyMapper;
 import org.junit.jupiter.api.*;
@@ -63,6 +64,9 @@ public class PlannerBindInviteFlowTest {
     @MockBean
     private GuideFamilyMapper guideFamilyMapper;
 
+    @MockBean
+    private MembershipService membershipService;
+
     // ========== 测试数据 ==========
 
     private static final Long GUIDE_USER_ID = 4001L;
@@ -183,10 +187,11 @@ public class PlannerBindInviteFlowTest {
 
         when(guideFamilyService.confirmBind(eq(GUIDE_USER_ID), eq(FAMILY_ID), eq("学业规划"), eq(29900)))
                 .thenReturn(true);
+        when(membershipService.getUserFamilyId(PARENT_USER_ID)).thenReturn(FAMILY_ID);
 
         // === When:家长点击"确认绑定" ===
         Result<Boolean> result = bindController.acceptBind(
-                PARENT_USER_ID, FAMILY_ID, bindDTO);
+                PARENT_USER_ID, bindDTO);
 
         // === Then:Milestone-3 - 绑定成功 ===
         assertEquals(200, result.getCode());
@@ -209,10 +214,11 @@ public class PlannerBindInviteFlowTest {
 
         when(guideFamilyService.confirmBind(eq(GUIDE_USER_ID), eq(FAMILY_ID), anyString(), anyInt()))
                 .thenThrow(new RuntimeException("该规划师已在服务中,无需重复绑定"));
+        when(membershipService.getUserFamilyId(PARENT_USER_ID)).thenReturn(FAMILY_ID);
 
         // === When:家长尝试再次绑定 ===
         Result<Boolean> result = bindController.acceptBind(
-                PARENT_USER_ID, FAMILY_ID, bindDTO);
+                PARENT_USER_ID, bindDTO);
 
         // === Then:返回错误提示 ===
         assertEquals(400, result.getCode());
@@ -236,9 +242,10 @@ public class PlannerBindInviteFlowTest {
         );
 
         when(guideFamilyService.getGuidesByFamily(FAMILY_ID)).thenReturn(mockBindings);
+        when(membershipService.getUserFamilyId(PARENT_USER_ID)).thenReturn(FAMILY_ID);
 
         // === When:家长请求规划师列表 ===
-        Result<List<Map<String, Object>>> result = bindController.getBoundGuides(FAMILY_ID);
+        Result<List<Map<String, Object>>> result = bindController.getBoundGuides(PARENT_USER_ID);
 
         // === Then:Milestone-4 - 返回已绑定规划师列表 ===
         assertEquals(200, result.getCode());
@@ -264,10 +271,11 @@ public class PlannerBindInviteFlowTest {
         GuideFamily unbindGuide = createGuideFamily(BINDING_ID, GUIDE_USER_ID, "学业规划", 29900, "unbound");
         when(guideFamilyService.unbindAndGet(eq(BINDING_ID), eq(FAMILY_ID))).thenReturn(unbindGuide);
         doNothing().when(teacherMessageService).sendMessage(eq(FAMILY_ID), eq(PARENT_USER_ID), eq(GUIDE_USER_ID), anyString());
+        when(membershipService.getUserFamilyId(PARENT_USER_ID)).thenReturn(FAMILY_ID);
 
         // === When:家长确认解除绑定 ===
         Result<Boolean> result = bindController.unbind(
-                PARENT_USER_ID, FAMILY_ID, unbindRequest);
+                PARENT_USER_ID, unbindRequest);
 
         // === Then:Milestone-5 - 解绑成功并通知规划师 ===
         assertEquals(200, result.getCode());
@@ -346,8 +354,9 @@ public class PlannerBindInviteFlowTest {
 
         when(guideFamilyService.confirmBind(eq(GUIDE_USER_ID), eq(FAMILY_ID), eq("学业规划"), eq(29900)))
                 .thenReturn(true);
+        when(membershipService.getUserFamilyId(PARENT_USER_ID)).thenReturn(FAMILY_ID);
 
-        Result<Boolean> step3 = bindController.acceptBind(PARENT_USER_ID, FAMILY_ID, bindDTO);
+        Result<Boolean> step3 = bindController.acceptBind(PARENT_USER_ID, bindDTO);
         assertEquals(200, step3.getCode());
         assertTrue(step3.getData());
     }

+ 138 - 4
cfc-frontend/pages/dan-assessment/report-upload.vue

@@ -15,17 +15,40 @@
         <view class="dimension-options">
           <view
             :class="['dimension-option', dimension === 'mind' ? 'active' : '']"
-            @click="dimension = 'mind'">
+            @click="selectDimension('mind')">
             <text class="dimension-name">&#x2764; 心维度 (A2)</text>
           </view>
           <view
             :class="['dimension-option', dimension === 'wisdom' ? 'active' : '']"
-            @click="dimension = 'wisdom'">
+            @click="selectDimension('wisdom')">
             <text class="dimension-name">&#x1F9E0; 智维度 (B4)</text>
           </view>
         </view>
       </view>
 
+      <!-- 购买推荐 -->
+      <view class="purchase-section" v-if="recommendedProducts.length > 0">
+        <view class="purchase-header">
+          <text class="purchase-icon">📦</text>
+          <text class="purchase-title">为家人购买检测服务</text>
+        </view>
+        <scroll-view class="purchase-scroll" scroll-x enable-flex show-scrollbar="false">
+          <view
+            class="purchase-card"
+            v-for="item in recommendedProducts"
+            :key="item.id"
+            @click="goToProduct(item.id)"
+          >
+            <image class="purchase-cover" :src="item.coverImage || '/static/default-product.png'" mode="aspectFill" />
+            <text class="purchase-name">{{ item.name }}</text>
+            <text class="purchase-price">{{ formatPriceWithSymbol(item.price) }}</text>
+            <view class="purchase-btn">
+              <text class="purchase-btn-text">去购买</text>
+            </view>
+          </view>
+        </scroll-view>
+      </view>
+
       <view class="upload-area" @click="chooseFile">
         <view class="upload-icon" v-if="!selectedFile">&#x1F4C4;</view>
         <view class="upload-icon" v-else>&#x2705;</view>
@@ -117,7 +140,12 @@
 </template>
 
 <script>
-import { danParsePreview, danConfirmReport } from '../../utils/api.js'
+import { danParsePreview, danConfirmReport, productList } from '../../utils/api.js'
+
+var REPORT_DOMAIN_MAP = {
+  mind: 'mind',
+  wisdom: 'wisdom'
+}
 
 export default {
   data() {
@@ -128,9 +156,14 @@ export default {
       previewData: null,
       draftId: null,
       loading: false,
-      confirming: false
+      confirming: false,
+      recommendedProducts: [],
+      loadingProducts: false
     }
   },
+  onLoad: function() {
+    this.loadRecommendProducts(this.dimension)
+  },
   methods: {
     goBack: function() {
       uni.navigateBack()
@@ -215,6 +248,37 @@ export default {
       this.selectedFile = null
       this.previewData = null
       this.draftId = null
+    },
+    selectDimension: function(dim) {
+      this.dimension = dim
+      this.loadRecommendProducts(dim)
+    },
+    loadRecommendProducts: function(dimension) {
+      var domain = REPORT_DOMAIN_MAP[dimension]
+      if (!domain) {
+        this.recommendedProducts = []
+        return
+      }
+      var self = this
+      self.loadingProducts = true
+      productList({ domain: domain, productType: 'assessment', size: 4 }).then(function(res) {
+        if (res && res.data && res.data.records) {
+          self.recommendedProducts = res.data.records
+        } else {
+          self.recommendedProducts = []
+        }
+      }).catch(function() {
+        self.recommendedProducts = []
+      }).finally(function() {
+        self.loadingProducts = false
+      })
+    },
+    goToProduct: function(productId) {
+      uni.navigateTo({ url: '/pages/discover/product-detail/product-detail?id=' + productId })
+    },
+    formatPriceWithSymbol: function(cents) {
+      if (cents === null || cents === undefined) return '¥0'
+      return '¥' + (cents / 100).toFixed(2)
     }
   }
 }
@@ -502,4 +566,74 @@ export default {
   font-size: 30rpx;
   color: #333;
 }
+.purchase-section {
+  background: #F0F9FF;
+  border-radius: 16rpx;
+  padding: 24rpx;
+  margin-top: 24rpx;
+  border: 2rpx solid #BAE6FD;
+}
+.purchase-header {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  margin-bottom: 16rpx;
+}
+.purchase-icon {
+  font-size: 28rpx;
+  margin-right: 8rpx;
+}
+.purchase-title {
+  font-size: 26rpx;
+  font-weight: bold;
+  color: #0369A1;
+}
+.purchase-scroll {
+  display: flex;
+  flex-direction: row;
+  white-space: nowrap;
+}
+.purchase-card {
+  display: inline-flex;
+  flex-direction: column;
+  width: 220rpx;
+  background: #fff;
+  border-radius: 12rpx;
+  padding: 16rpx;
+  margin-right: 16rpx;
+  flex-shrink: 0;
+}
+.purchase-cover {
+  width: 188rpx;
+  height: 120rpx;
+  border-radius: 8rpx;
+  background: #F1F5F9;
+  margin-bottom: 8rpx;
+}
+.purchase-name {
+  font-size: 22rpx;
+  color: #333;
+  line-height: 1.3;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  margin-bottom: 4rpx;
+}
+.purchase-price {
+  font-size: 24rpx;
+  color: #EF4444;
+  font-weight: bold;
+  margin-bottom: 8rpx;
+}
+.purchase-btn {
+  background: #3B82F6;
+  border-radius: 8rpx;
+  padding: 8rpx 0;
+  text-align: center;
+}
+.purchase-btn-text {
+  font-size: 22rpx;
+  color: #fff;
+  font-weight: 500;
+}
 </style>

+ 1 - 1
cfc-web/.last_build_commit

@@ -1 +1 @@
-3ef6261f71abb5f44cc409edaa763aad94df7700
+ed888e1a12071e54e29bcd4a14f47d2ea6268a39

+ 2 - 2
cfc-web/package-lock.json

@@ -1,12 +1,12 @@
 {
   "name": "cfc-web",
-  "version": "1.0.405",
+  "version": "1.0.406",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "cfc-web",
-      "version": "1.0.405",
+      "version": "1.0.406",
       "dependencies": {
         "@wangeditor/editor": "^5.1.23",
         "@wangeditor/editor-for-vue": "^1.0.2",

+ 1 - 1
cfc-web/package.json

@@ -1,6 +1,6 @@
 {
   "name": "cfc-web",
-  "version": "1.0.405",
+  "version": "1.0.406",
   "private": true,
   "scripts": {
     "dev": "vue-cli-service serve",

+ 20 - 0
cfc-web/public/CHANGELOG-v1.0.md

@@ -4,6 +4,26 @@
 
 ---
 
+## v1.0.406 (2026-07-19)
+
+### Bug 修复
+- resolve @RequestAttribute familyId 400 error by resolving via MembershipService
+- replace @RequestAttribute("familyId") with
+
+### 其他
+- no interceptor sets familyId as a request attribute → Spring 400 error.
+- @RequestAttribute("userId") + membershipService.getUserFamilyId(userId)
+- Affected controllers:
+- - MindFortuneController (getTodayFortune, refreshFortune, exportFortuneReport)
+- - PackagePaymentController (createOrder)
+- - MarketController (purchasePackage, getMyPlans)
+- - BindController (acceptBind, getBoundGuides, unbind)
+- 
+
+### 新功能
+- add purchase recommendation section to dan-assessment/report-upload page
+
+
 ## v1.0.405 (2026-07-19)
 
 ### Bug 修复

+ 21 - 1
cfc-web/public/CHANGELOG.md

@@ -1,6 +1,6 @@
 # 更新日志
 
-> 当前版本: v1.0.405
+> 当前版本: v1.0.406
 
 ## 历史版本
 
@@ -8,6 +8,26 @@
 
 ---
 
+## v1.0.406 (2026-07-19)
+
+### Bug 修复
+- resolve @RequestAttribute familyId 400 error by resolving via MembershipService
+- replace @RequestAttribute("familyId") with
+
+### 其他
+- no interceptor sets familyId as a request attribute → Spring 400 error.
+- @RequestAttribute("userId") + membershipService.getUserFamilyId(userId)
+- Affected controllers:
+- - MindFortuneController (getTodayFortune, refreshFortune, exportFortuneReport)
+- - PackagePaymentController (createOrder)
+- - MarketController (purchasePackage, getMyPlans)
+- - BindController (acceptBind, getBoundGuides, unbind)
+- 
+
+### 新功能
+- add purchase recommendation section to dan-assessment/report-upload page
+
+
 ## v1.0.405 (2026-07-19)
 
 ### Bug 修复