Browse Source

feat(article): 完成阅读且获得CF值后从文章列表中隐藏

- ArticleReadingRecordMapper: selectCompletedArticleIdsByChild 改为双条件 SQL(reading_completed=1 AND quiz cf_earned>0)
- ArticleService.getPublicList: 新增 memberId 参数,权限过滤后移除已读完+得CF的文章
- ArticleController.list: 读取 body.memberId(回退 currentMemberId)并传入 service
- article-center/index.vue: 传 memberId 给列表接口,知识分享页隐藏已完成文章
- mind-detail/articles.vue: 同上,心智知识页同步过滤
- ArticleControllerTest: 适配 getPublicList 新增 memberId 参数
Sisyphus 2 days ago
parent
commit
cf04db36e5

+ 4 - 2
cfc-backend/src/main/java/com/etotem/cfc/controller/content/ArticleController.java

@@ -36,10 +36,12 @@ public class ArticleController {
 
     @PostMapping("/list")
     public Result<Page<Article>> list(@RequestBody Map<String, Object> body,
-                                       @RequestAttribute(value = "userId", required = false) Long userId) {
+                                       @RequestAttribute(value = "userId", required = false) Long userId,
+                                       @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId) {
         Long categoryId = ParamUtils.getLong(body.get("categoryId"));
         String keyword = (String) body.get("keyword");
         String dimensionCode = body.get("dimensionCode") != null ? body.get("dimensionCode").toString() : null;
+        Long memberId = ParamUtils.getLong(body.get("memberId"), currentMemberId);
         int page = body.get("page") != null ? Integer.parseInt(body.get("page").toString()) : 1;
         int size = body.get("size") != null ? Integer.parseInt(body.get("size").toString()) : 10;
 
@@ -54,7 +56,7 @@ public class ArticleController {
         }
 
         Page<Article> result = articleService.getPublicList(categoryId, keyword, dimensionCode, page, size,
-                userId, familyId, vendorType);
+                userId, familyId, vendorType, memberId);
         return Result.success(result);
     }
 

+ 6 - 3
cfc-backend/src/main/java/com/etotem/cfc/mapper/ArticleReadingRecordMapper.java

@@ -49,9 +49,12 @@ public interface ArticleReadingRecordMapper extends BaseMapper<ArticleReadingRec
     long countDistinctByArticle(@Param("articleId") Long articleId);
 
     /**
-     * 查询某用户已达标阅读的文章 ID 列表
+     * 查询某成员已完成阅读且获得CF值的文章 ID 列表
+     * (阅读达标 reading_completed=1 + 答题获得CF值 cf_earned>0 才从列表中隐藏)
      */
-    @Select("SELECT article_id FROM article_reading_records " +
-            "WHERE child_id = #{childId} AND reading_completed = 1")
+    @Select("SELECT DISTINCT r.article_id FROM article_reading_records r " +
+            "WHERE r.child_id = #{childId} AND r.reading_completed = 1 " +
+            "AND EXISTS (SELECT 1 FROM article_quiz_records q " +
+            "            WHERE q.article_id = r.article_id AND q.member_id = r.child_id AND q.cf_earned > 0)")
     List<Long> selectCompletedArticleIdsByChild(@Param("childId") Long childId);
 }

+ 15 - 2
cfc-backend/src/main/java/com/etotem/cfc/service/ArticleService.java

@@ -86,6 +86,11 @@ public class ArticleService {
 
     public Page<Article> getPublicList(Long categoryId, String keyword, String dimensionCode, int page, int size,
                                         Long userId, Long familyId, String vendorType) {
+        return getPublicList(categoryId, keyword, dimensionCode, page, size, userId, familyId, vendorType, null);
+    }
+
+    public Page<Article> getPublicList(Long categoryId, String keyword, String dimensionCode, int page, int size,
+                                        Long userId, Long familyId, String vendorType, Long memberId) {
         LambdaQueryWrapper<Article> wrapper = new LambdaQueryWrapper<Article>()
                 .eq(Article::getStatus, "published")
                 .orderByDesc(Article::getIsFeatured)
@@ -103,8 +108,8 @@ public class ArticleService {
             String dim = dimensionCode.trim();
             // P1: 权重感知查询 — 匹配 dimension_weights > 0 或兼容旧版 relatedDimensions
             wrapper.and(w -> w
-                .apply("COALESCE(JSON_EXTRACT(dimension_weights, CONCAT('$.', {0})), 0) > 0", dim)
-                .or().like(Article::getRelatedDimensions, dim)
+                    .apply("COALESCE(JSON_EXTRACT(dimension_weights, CONCAT('$.', {0})), 0) > 0", dim)
+                    .or().like(Article::getRelatedDimensions, dim)
             );
         }
 
@@ -116,6 +121,14 @@ public class ArticleService {
 
         List<Article> visible = articlePermissionService.filterVisible(all, userId, familyId, vendorType);
 
+        // 过滤已完成阅读且获得CF值的文章(达到阈值且答题得CF后不再出现在文章列表中)
+        if (memberId != null) {
+            List<Long> completedIds = articleReadingRecordMapper.selectCompletedArticleIdsByChild(memberId);
+            if (!completedIds.isEmpty()) {
+                visible.removeIf(a -> completedIds.contains(a.getId()));
+            }
+        }
+
         enrichTotalDurationSeconds(visible);
 
         // 手动分页(与 getFeatured 相同模式),total 为过滤后的真实总数

+ 6 - 6
cfc-backend/src/test/java/com/etotem/cfc/integration/controller/ArticleControllerTest.java

@@ -65,7 +65,7 @@ public class ArticleControllerTest {
         when(articleService.getPublicList(isNull(), isNull(), isNull(), eq(1), eq(10), isNull(), isNull(), isNull()))
             .thenReturn(mockPage);
 
-        Result<Page<Article>> result = controller.list(body, null);
+        Result<Page<Article>> result = controller.list(body, null, null);
         assertEquals(200, result.getCode());
         assertNotNull(result.getData());
         assertEquals(1, result.getData().getRecords().size());
@@ -85,7 +85,7 @@ public class ArticleControllerTest {
         when(articleService.getPublicList(eq(1L), isNull(), isNull(), eq(1), eq(10), isNull(), isNull(), isNull()))
             .thenReturn(mockPage);
 
-        Result<Page<Article>> result = controller.list(body, null);
+        Result<Page<Article>> result = controller.list(body, null, null);
         assertEquals(200, result.getCode());
         assertEquals(1, result.getData().getRecords().size());
     }
@@ -104,7 +104,7 @@ public class ArticleControllerTest {
         when(articleService.getPublicList(isNull(), eq("测试"), isNull(), eq(1), eq(10), isNull(), isNull(), isNull()))
             .thenReturn(mockPage);
 
-        Result<Page<Article>> result = controller.list(body, null);
+        Result<Page<Article>> result = controller.list(body, null, null);
         assertEquals(200, result.getCode());
         assertEquals("测试文章", result.getData().getRecords().get(0).getTitle());
     }
@@ -127,7 +127,7 @@ public class ArticleControllerTest {
         when(articleService.getPublicList(isNull(), isNull(), isNull(), eq(1), eq(10), eq(1L), eq(100L), eq("planner")))
             .thenReturn(mockPage);
 
-        Result<Page<Article>> result = controller.list(body, 1L);
+        Result<Page<Article>> result = controller.list(body, 1L, null);
         assertEquals(200, result.getCode());
         assertNotNull(result.getData());
     }
@@ -172,7 +172,7 @@ public class ArticleControllerTest {
         );
         when(articleService.getFeatured(eq(3), isNull(), isNull(), isNull(), isNull())).thenReturn(featured);
 
-        Result<List<Article>> result = controller.featured(body, null);
+        Result<List<Article>> result = controller.featured(body, null, null);
         assertEquals(200, result.getCode());
         assertEquals(2, result.getData().size());
     }
@@ -185,7 +185,7 @@ public class ArticleControllerTest {
         // Default size should be 5
         when(articleService.getFeatured(eq(5), isNull(), isNull(), isNull(), isNull())).thenReturn(featured);
 
-        Result<List<Article>> result = controller.featured(body, null);
+        Result<List<Article>> result = controller.featured(body, null, null);
         assertEquals(200, result.getCode());
         assertEquals(1, result.getData().size());
     }

+ 6 - 2
cfc-frontend/pages/article-center/index.vue

@@ -165,15 +165,16 @@ export default {
   components: {
     ArticleBookshelf: () => require('@/components/ArticleBookshelf.vue')
   },
-  onLoad(options) {
+onLoad(options) {
     // 支持从外部传入 dimension 参数,默认选中对应 tab
     if (options && options.dimension) {
       this.currentTab = options.dimension
     }
     this.isLoggedIn = !!uni.getStorageSync('token')
+    this.currentChildId = uni.getStorageSync('currentChildId') || null
     this.loadArticles()
     this.loadRecommendedArticles()
-  },
+},
   onShow() {
     this.isLoggedIn = !!uni.getStorageSync('token')
   },
@@ -191,6 +192,9 @@ export default {
       }
       try {
         var params = { page: this.page, size: this.size }
+        if (this.currentChildId) {
+          params.memberId = parseInt(this.currentChildId)
+        }
         if (this.currentTab) {
           params.dimensionCode = this.currentTab
         }

+ 6 - 2
cfc-frontend/pages/mind-detail/articles.vue

@@ -138,9 +138,10 @@ export default {
     }
     this.loadCategories()
   },
-  onShow: function() {
+onShow: function() {
     this.isLoggedIn = !!uni.getStorageSync('token')
-  },
+    this.currentChildId = uni.getStorageSync('currentChildId') || null
+},
   methods: {
     contentTypeLabel: function(type) {
       var labels = { article: '文章', knowledge: '知识点', course: '课程', tip: '贴士' }
@@ -187,6 +188,9 @@ export default {
       var self = this
       try {
         var params = { page: this.page, size: this.size }
+        if (this.currentChildId) {
+          params.memberId = parseInt(this.currentChildId)
+        }
         if (this.currentCategory) {
           params.categoryId = this.currentCategory
         }