Jelajahi Sumber

docs: 推荐组件移除维度实现计划

Xiaogang Liao 1 bulan lalu
induk
melakukan
57b417f84c

+ 716 - 0
docs/superpowers/plans/2026-08-09-dimension-recommendation-remove.md

@@ -0,0 +1,716 @@
+# 推荐组件移除维度参数 实现计划
+
+> **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:subagent-driven-development。步骤使用复选框(`- [ ]`)语法来跟踪进度。
+
+**目标:** 移除 DimensionProducts/Articles/Activities/Tasks 四个组件的 dimensionCode 参数,组件改为自加载全量推荐内容,父页面不再传维度参数也不负责数据加载。
+
+**架构:** 组件自加载模式。后端 `/api/recommend/dimension-products` 支持空 dimensionCode 返回全量推荐。Article/Activity/Task 后端已支持空过滤参数,无需改动。11 个父页面移除维度传参和本地数据加载。
+
+**技术栈:** Java 8 + Spring Boot 2.7.18 / uni-app Vue 2 + 微信小程序
+
+---
+
+### 任务 1:后端 ProductRecommendationController 支持空 dimensionCode
+
+**文件:**
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/controller/recommendation/ProductRecommendationController.java`
+
+- [ ] **步骤 1:修改 Controller 校验逻辑**
+
+将 dimensionCode 为空时返回错误的逻辑改为允许空值,调用 service 时用 null:
+
+```java
+@Operation(summary = "获取推荐商品")
+@PostMapping("/dimension-products")
+public Result<List<Map<String, Object>>> getDimensionProducts(@RequestBody Map<String, Object> params,
+                                                              @RequestAttribute(value = "familyId", required = false) Long familyId) {
+    if (familyId == null) {
+        return Result.noFamily("请先创建或加入家庭");
+    }
+    // 去掉 dimensionCode 非空校验,允许 null 返回全量推荐
+    String dimensionCode = (String) params.get("dimensionCode");
+
+    Object memberIdObj = params.get("memberId");
+    Long memberId = memberIdObj != null ? toLong(memberIdObj) : null;
+
+    @SuppressWarnings("unchecked")
+    List<Long> excludeIds = (List<Long>) params.get("excludeProductIds");
+
+    Object limitObj = params.get("limit");
+    int limit = limitObj != null ? toInt(limitObj) : 6;
+
+    List<Map<String, Object>> result = productRecommendationService.getDimensionRecommendations(
+            dimensionCode, familyId, memberId, excludeIds, limit);
+
+    return Result.success(result);
+}
+```
+
+- [ ] **步骤 2:Commit**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/controller/recommendation/ProductRecommendationController.java
+git commit -m "fix: 推荐商品接口支持空 dimensionCode 返回全量"
+```
+
+---
+
+### 任务 2:后端 ProductRecommendationService 全量查询逻辑
+
+**文件:**
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/service/ProductRecommendationService.java`
+
+- [ ] **步骤 1:修改 getDimensionRecommendations 方法 — mapping 查询支持 null dimensionCode**
+
+将第 62-67 行的 mapping 查询改为 conditionally 加 dimensionCode 条件:
+
+```java
+// 从 mapping 表获取关联商品
+LambdaQueryWrapper<ProductDimensionMapping> mappingQw = new LambdaQueryWrapper<ProductDimensionMapping>()
+        .eq(ProductDimensionMapping::getEnabled, 1);
+if (dimensionCode != null && !dimensionCode.isEmpty()) {
+    mappingQw.eq(ProductDimensionMapping::getDimensionCode, dimensionCode);
+}
+List<ProductDimensionMapping> mappings = dimensionMappingMapper.selectList(mappingQw);
+```
+
+- [ ] **步骤 2:修改 fallback domain 查询支持 null**
+
+将第 107-118 行 fallback 查询改为 conditionally 加 domain 条件:
+
+```java
+// fallback: 从 Product.domain 匹配
+if (candidates.isEmpty()) {
+    LambdaQueryWrapper<Product> domainQw = new LambdaQueryWrapper<Product>()
+            .eq(Product::getStatus, "上架")
+            .gt(Product::getStock, 0)
+            .orderByAsc(Product::getSortOrder);
+    if (dimensionCode != null && !dimensionCode.isEmpty()) {
+        domainQw.eq(Product::getDomain, dimensionCode);
+    }
+    if (!purchasedIds.isEmpty()) {
+        domainQw.notIn(Product::getId, purchasedIds);
+    }
+    List<Product> domainMatched = productMapper.selectList(domainQw);
+    candidates.addAll(domainMatched);
+}
+```
+
+- [ ] **步骤 3:修改 dimensionBoost / matchScore / reason 方法 — 空 dimensionCode 时使用通用逻辑**
+
+将 `applyDimensionBoost` 方法修改为:
+
+```java
+private double applyDimensionBoost(int baseScore, String dimensionCode, Map<String, Integer> memberScores) {
+    if (dimensionCode == null || dimensionCode.isEmpty()) {
+        return baseScore;
+    }
+    Integer score = memberScores.get(dimensionCode);
+    if (score == null || score >= 70) {
+        return baseScore;
+    } else if (score >= 50) {
+        return baseScore * 1.1;
+    } else {
+        return baseScore * 1.2;
+    }
+}
+```
+
+将 `calculateMatchScore` 方法修改为:
+
+```java
+private double calculateMatchScore(Product product, String dimensionCode, Map<String, Integer> memberScores) {
+    int base = 70;
+    if (dimensionCode == null || dimensionCode.isEmpty()) {
+        // 无维度时,用最低分维度作为参考
+        Integer minScore = memberScores.values().stream().min(Integer::compareTo).orElse(null);
+        if (minScore != null) {
+            if (minScore < 50) base = 90;
+            else if (minScore < 70) base = 80;
+        }
+        return base;
+    }
+    Integer myScore = memberScores.get(dimensionCode);
+    if (myScore != null) {
+        if (myScore < 50) base = 90;
+        else if (myScore < 70) base = 80;
+        else base = 70;
+    }
+    return base;
+}
+```
+
+将 `buildRecommendationReason` 方法修改为:
+
+```java
+private String buildRecommendationReason(Product product, String dimensionCode, Map<String, Integer> memberScores) {
+    if (dimensionCode == null || dimensionCode.isEmpty()) {
+        return "为您推荐的好物";
+    }
+    Integer score = memberScores.get(dimensionCode);
+    if (score == null) {
+        return "根据您的维度匹配为您推荐";
+    }
+    if (score < 50) {
+        return "该维度得分偏低,重点推荐";
+    } else if (score < 70) {
+        return "该维度有提升空间,推荐关注";
+    }
+    return "丰富您的维度生活";
+}
+```
+
+- [ ] **步骤 4:编译验证**
+
+运行:
+```bash
+cd cfc-backend && mvn clean compile
+```
+
+预期:编译通过,无错误。
+
+- [ ] **步骤 5:Commit**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/service/ProductRecommendationService.java
+git commit -m "feat: 商品推荐支持空维度返回全量推荐"
+```
+
+---
+
+### 任务 3:DimensionProducts.vue — 移除 dimensionCode,自加载
+
+**文件:**
+- 修改:`cfc-frontend/components/DimensionProducts.vue`
+
+- [ ] **步骤 1:移除 dimensionCode prop**
+
+```js
+props: {
+    // 移除 dimensionCode
+    products: { type: Array, default: function() { return [] } },
+    familyId: { type: [Number, String], default: null },
+    isLoggedIn: { type: Boolean, default: false },
+    title: { type: String, default: '推荐商品' },
+    showMore: { type: Boolean, default: true }
+},
+```
+
+- [ ] **步骤 2:修改 loadDimensionProducts 方法,去掉 dimensionCode 参数**
+
+```js
+loadDimensionProducts: function() {
+    var self = this
+    self.loading = true
+    var params = {
+        limit: 4
+    }
+    getDimensionProducts(params).then(function(res) {
+        self.loading = false
+        if (res.code === 200 && res.data) {
+            self.selfProducts = Array.isArray(res.data) ? res.data : (res.data.records || [])
+        }
+    }).catch(function() {
+        self.loading = false
+        self.selfProducts = []
+    })
+}
+```
+
+- [ ] **步骤 3:Commit**
+
+```bash
+git add cfc-frontend/components/DimensionProducts.vue
+git commit -m "fix: DimensionProducts 移除维度参数自加载"
+```
+
+---
+
+### 任务 4:DimensionArticles.vue — 移除 dimensionCode,新增自加载
+
+**文件:**
+- 修改:`cfc-frontend/components/DimensionArticles.vue`
+
+- [ ] **步骤 1:添加 API 导入**
+
+在 script 顶部添加:
+```js
+import { getFeaturedArticles } from '@/utils/api.js'
+```
+
+- [ ] **步骤 2:移除 dimensionCode prop**
+
+```js
+props: {
+    // 移除 dimensionCode
+    articles: { type: Array, default: function() { return [] } },
+    isLoggedIn: { type: Boolean, default: false },
+    colors: { type: Array, default: function() { return defaultGradientColors } }
+},
+```
+
+- [ ] **步骤 3:添加 data 和 self-loading**
+
+```js
+data: function() {
+    return {
+        selfArticles: [],
+        loading: false
+    }
+},
+mounted: function() {
+    if (!this.articles || this.articles.length === 0) {
+        this.loadArticles()
+    }
+},
+```
+
+- [ ] **步骤 4:修改 displayArticles computed,优先用 selfArticles**
+
+```js
+displayArticles: function() {
+    var self = this
+    var list = (this.articles && this.articles.length > 0 ? this.articles : self.selfArticles)
+    return (list || []).map(function(item) {
+        item._badges = self.parseWeights(item.dimensionWeights)
+        return item
+    })
+}
+```
+
+- [ ] **步骤 5:添加 loadArticles 方法**
+
+```js
+loadArticles: function() {
+    var self = this
+    self.loading = true
+    getFeaturedArticles({ size: 3 }).then(function(res) {
+        self.loading = false
+        if (res && res.code === 200 && res.data) {
+            var list = Array.isArray(res.data) ? res.data : (res.data.records || [])
+            self.selfArticles = list.slice(0, 3)
+        }
+    }).catch(function() {
+        self.loading = false
+        self.selfArticles = []
+    })
+}
+```
+
+- [ ] **步骤 6:Commit**
+
+```bash
+git add cfc-frontend/components/DimensionArticles.vue
+git commit -m "fix: DimensionArticles 移除维度参数自加载"
+```
+
+---
+
+### 任务 5:DimensionActivities.vue — 移除 dimensionCode,新增自加载
+
+**文件:**
+- 修改:`cfc-frontend/components/DimensionActivities.vue`
+
+- [ ] **步骤 1:添加 API 导入**
+
+在 script 顶部添加:
+```js
+import { getActivityList } from '@/utils/api.js'
+```
+
+- [ ] **步骤 2:移除 dimensionCode prop 和 dimensionIcon computed**
+
+```js
+props: {
+    // 移除 dimensionCode
+    activities: { type: Array, default: function() { return [] } },
+    isLoggedIn: { type: Boolean, default: false }
+}
+```
+
+删除 `dimensionIcon` computed。
+
+- [ ] **步骤 3:修改 template 中的图标为固定值**
+
+将 `{{ dimensionIcon }}` 替换为固定图标 `'🎯'`:
+
+```vue
+<!-- 头部图标 -->
+<view class="section-icon-wrap">
+    <text class="section-icon">🎯</text>
+</view>
+
+<!-- 空状态图标 -->
+<text class="empty-icon">🎯</text>
+```
+
+- [ ] **步骤 4:添加 data 和 self-loading**
+
+```js
+data: function() {
+    return {
+        swiperIndex: 0,
+        isMember: false,
+        selfActivities: [],
+        loading: false
+    }
+},
+mounted: function() {
+    if (!this.activities || this.activities.length === 0) {
+        this.loadActivities()
+    }
+}
+```
+
+- [ ] **步骤 5:修改 displayActivities computed,优先用 selfActivities**
+
+```js
+displayActivities: function() {
+    var self = this
+    var list = (this.activities && this.activities.length > 0 ? this.activities : self.selfActivities)
+    return (list || []).slice(0, 3).map(function(item) {
+        return {
+            id: item.id,
+            title: item.title,
+            coverImage: item.coverImage,
+            startTime: item.startTime,
+            status: item.status,
+            price: item.price,
+            priceLabel: item.priceLabel,
+            memberPrice: item.memberPrice,
+            dimensionWeights: item.dimensionWeights,
+            _badges: self.parseWeights(item.dimensionWeights),
+            _timeText: self.formatTime(item.startTime),
+            _statusText: self.statusText(item.status),
+            _priceDisplay: self.formatPriceWithSymbol(item.price),
+            _memberPriceDisplay: self.formatPriceWithSymbol(item.memberPrice)
+        }
+    })
+}
+```
+
+- [ ] **步骤 6:添加 loadActivities 方法**
+
+```js
+loadActivities: function() {
+    var self = this
+    self.loading = true
+    getActivityList({ page: 1, size: 3 }).then(function(res) {
+        self.loading = false
+        if (res && res.code === 200 && res.data) {
+            var list = Array.isArray(res.data) ? res.data : (res.data.records || [])
+            self.selfActivities = list.slice(0, 3)
+        }
+    }).catch(function() {
+        self.loading = false
+        self.selfActivities = []
+    })
+}
+```
+
+- [ ] **步骤 7:Commit**
+
+```bash
+git add cfc-frontend/components/DimensionActivities.vue
+git commit -m "fix: DimensionActivities 移除维度参数自加载"
+```
+
+---
+
+### 任务 6:DimensionTasks.vue — 移除 dimensionCode,改用 getTodayTasks
+
+**文件:**
+- 修改:`cfc-frontend/components/DimensionTasks.vue`
+
+- [ ] **步骤 1:添加 getTodayTasks 导入**
+
+```js
+import api from '@/utils/api.js'
+// 确保 api.getTodayTasks 可用(已存在于 api.js 第 453 行)
+```
+
+- [ ] **步骤 2:移除 dimensionCode prop**
+
+```js
+props: {
+    // 移除 dimensionCode
+    tasks: { type: Array, default: function() { return [] } },
+    memberId: { type: Number, default: null }
+},
+```
+
+- [ ] **步骤 3:修改 loadTasks 方法,使用 getTodayTasks(不带 category)**
+
+```js
+loadTasks: function() {
+    var self = this
+    if (!self.memberId) return
+    self.loading = true
+    api.getTodayTasks(self.memberId).then(function(res) {
+        self.loading = false
+        self.tasksData = Array.isArray(res.data) ? res.data : (res.data.records || [])
+    }).catch(function() {
+        self.loading = false
+        self.tasksData = []
+    })
+}
+```
+
+- [ ] **步骤 4:Commit**
+
+```bash
+git add cfc-frontend/components/DimensionTasks.vue
+git commit -m "fix: DimensionTasks 移除维度参数自加载"
+```
+
+---
+
+### 任务 7:父页面清理 — 7 个维度详情页(body/mind/wisdom/action 两个版本)
+
+**文件:**
+- 修改:`cfc-frontend/pages/body/member-body-detail.vue`
+- 修改:`cfc-frontend/pages/mind/member-mind-detail.vue`
+- 修改:`cfc-frontend/pages/wisdom/member-wisdom-detail.vue`
+- 修改:`cfc-frontend/pages/action-detail/member-action-detail.vue`
+- 修改:`cfc-frontend/pages/body-detail/member-body-detail.vue`
+- 修改:`cfc-frontend/pages/mind-detail/member-mind-detail.vue`
+- 修改:`cfc-frontend/pages/wisdom-detail/member-wisdom-detail.vue`
+
+**统一改动模式(每个页面):**
+
+- [ ] **步骤 1:去掉 import 中的 API**
+
+```js
+// 移除 getTodayTasksByCategory, getActivityList, getProductsByDomain
+import { getEnergyOverview, getFamilyEnergySandbox, getChildren } from '../../utils/api.js'
+```
+
+- [ ] **步骤 2:去掉 data 中的数据字段**
+
+```js
+data() {
+    return {
+        memberId: null,
+        selfId: null,
+        memberInfo: null,
+        sandboxData: null,
+        dualDimension: null,
+        // 移除 dimensionTasks, dimensionActivities, dimensionProducts
+        ...
+    }
+}
+```
+
+- [ ] **步骤 3:去掉 template 中的 dimensionCode 和 props**
+
+```vue
+<!-- 今日任务 -->
+<DimensionTasks
+    :memberId="memberId"
+    @taskClick="onTaskClick"
+    @moreTasks="goTasks" />
+
+<!-- 活动 -->
+<DimensionActivities
+    :isLoggedIn="true"
+    @activityClick="goActivityDetail"
+    @moreActivities="goMoreActivities" />
+
+<!-- 商品 -->
+<DimensionProducts
+    :isLoggedIn="true"
+    @productClick="goProductDetail"
+    @moreProducts="goMoreProducts" />
+```
+
+- [ ] **步骤 4:去掉 onShow 中的数据加载调用**
+
+```js
+// 移除 this.loadDimensionTasks()
+// 移除 this.loadDimensionActivities()
+// 移除 this.loadDimensionProducts()
+```
+
+- [ ] **步骤 5:去掉 loadDimensionTasks / loadDimensionActivities / loadDimensionProducts 方法**
+
+删除这三个方法定义。
+
+- [ ] **步骤 6:对 7 个页面逐一完成上述改动**
+
+每个页面完成后立即 commit:
+
+```bash
+git add cfc-frontend/pages/body/member-body-detail.vue
+git commit -m "fix: body 成员详情页移除维度参数"
+```
+
+按 body → mind → wisdom → action → body-detail → mind-detail → wisdom-detail 顺序逐个提交。
+
+---
+
+### 任务 8:index-home/index.vue — 去掉维度相关数据加载
+
+**文件:**
+- 修改:`cfc-frontend/pages/index-home/index.vue`
+
+- [ ] **步骤 1:去掉 DimensionProducts 和 DimensionArticles 的 dimensionCode prop**
+
+```vue
+<DimensionArticles
+    :articles="actionArticles"
+    :isLoggedIn="true"
+    @articleClick="goArticleDetail" />
+
+<DimensionProducts
+    dimensionCode="action"  <!-- 去掉这行 -->
+    ... />
+```
+
+- [ ] **步骤 2:去掉 loadActionProducts 方法**
+
+删除 `loadActionProducts` 方法定义。
+
+- [ ] **步骤 3:去掉 loadActionArticles 方法**
+
+删除 `loadActionArticles` 方法定义。
+
+- [ ] **步骤 4:去掉 onShow 中的调用**
+
+```js
+// 移除 this.loadActionProducts()
+// 移除 this.loadActionArticles()
+// 移除 this.loadDimensionActivities()(growth-main 已传空 dimensionCode,可保留)
+```
+
+- [ ] **步骤 5:去掉 import 中的 getProductsByDomain, getFeaturedArticles**
+
+- [ ] **步骤 6:去掉 data 中的 actionArticles, dimensionProducts 字段**
+
+- [ ] **步骤 7:Commit**
+
+```bash
+git add cfc-frontend/pages/index-home/index.vue
+git commit -m "fix: index-home 移除维度参数"
+```
+
+---
+
+### 任务 9:member-detail.vue 和 home-pages/member-home-detail.vue
+
+**文件:**
+- 修改:`cfc-frontend/pages/member-detail/member-detail.vue`
+- 修改:`cfc-frontend/pages/home-pages/member-home-detail.vue`
+
+- [ ] **步骤 1(member-detail):去掉 DimensionTasks 的 dimensionCode="all" prop**
+
+```vue
+<DimensionTasks
+    v-if="memberId"
+    :memberId="memberId"
+    @taskClick="onTaskClick"
+    @moreTasks="goTasks" />
+```
+
+注意:`member-detail` 页面的 `this.dimensionCode` 数据属性用于页面导航逻辑(`onGraphMemberTap`),不要删除!只去掉组件的 prop 传参。
+
+- [ ] **步骤 2(member-detail):去掉 loadDimensionTasks 方法(如果存在)**
+
+检查页面是否调用 `getTodayTasksByCategory` 加载任务数据。如果有则删除。
+
+- [ ] **步骤 3(home-pages):去掉 DimensionTasks 的 dimensionCode="all" prop**
+
+```vue
+<DimensionTasks
+    v-if="memberId"
+    :memberId="memberId"
+    @taskClick="goTasks"
+    @moreTasks="goTasks" />
+```
+
+- [ ] **步骤 4(home-pages):去掉 loadData 中的任务加载逻辑**
+
+```js
+// 删除:
+// getTodayTasksByCategory(self.memberId, 'all').then(...)
+```
+
+- [ ] **步骤 5(home-pages):去掉 import 中的 getTodayTasksByCategory**
+
+- [ ] **步骤 6:Commit**
+
+```bash
+git add cfc-frontend/pages/member-detail/member-detail.vue cfc-frontend/pages/home-pages/member-home-detail.vue
+git commit -m "fix: member-detail 和 home-detail 移除任务维度参数"
+```
+
+---
+
+### 任务 10:growth-main/index.vue — 去掉 DimensionActivities 的 dimensionCode
+
+**文件:**
+- 修改:`cfc-frontend/pages/growth-main/index.vue`
+
+- [ ] **步骤 1:去掉 DimensionActivities 的 dimensionCode prop**
+
+```vue
+<DimensionActivities
+    :isLoggedIn="true"
+    @activityClick="goActivityDetail"
+    @moreActivities="goMoreActivities" />
+```
+
+- [ ] **步骤 2:去掉 loadDimensionActivities 方法和 onLoad 中的调用**
+
+```js
+// 删除 loadDimensionActivities 方法
+// 删除 onLoad 中的 this.loadDimensionActivities()
+```
+
+- [ ] **步骤 3:去掉 data 中的 dimensionActivities 字段**
+
+- [ ] **步骤 4:去掉 import 中的 getActivityList**
+
+- [ ] **步骤 5:Commit**
+
+```bash
+git add cfc-frontend/pages/growth-main/index.vue
+git commit -m "fix: growth-main 移除活动维度参数"
+```
+
+---
+
+### 任务 11:验证与最终提交
+
+- [ ] **步骤 1:后端编译验证**
+
+```bash
+cd cfc-backend && mvn clean compile
+```
+
+预期:编译通过。
+
+- [ ] **步骤 2:统计所有改动**
+
+```bash
+git diff --stat
+```
+
+预期:改动涵盖:
+- `cfc-backend/` — ProductRecommendationController + ProductRecommendationService
+- `cfc-frontend/components/` — 4 个组件
+- `cfc-frontend/pages/` — 11 个页面
+
+- [ ] **步骤 3:最终 Commit**
+
+```bash
+git add -A
+git commit -m "fix: 推荐组件移除维度参数(商品/文章/活动/任务)"
+```
+
+- [ ] **步骤 4:确认**
+
+运行:
+```bash
+git diff HEAD~1 --stat
+```
+确认改动范围正确。