Browse Source

chore: auto bump version and changelog [skip ci]

iwt 1 month ago
parent
commit
0b2c1a612c

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

@@ -11202,5 +11202,8 @@ log.info("迁移298: 已为无 openid 的 child 账号补齐 family_members 记
         } catch (Exception e) {
             log.warn("迁移314: 回填 article_reading_records.article_id 失败: {}", e.getMessage());
         }
+
+        // 迁移315: article_reading_records 添加 reading_completed 列(阅读达标后标记,用于推荐过滤)
+        ensureColumn("article_reading_records", "reading_completed", "TINYINT DEFAULT 0 COMMENT '是否已阅读达标(达到最低时长阈值)'");
     }
 }

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

@@ -83,7 +83,8 @@ public class ArticleController {
 
     @PostMapping("/featured")
     public Result<List<Article>> featured(@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) {
         int size = body.get("size") != null ? Integer.parseInt(body.get("size").toString()) : 5;
         String dimensionCode = body.get("dimensionCode") != null ? body.get("dimensionCode").toString() : null;
 
@@ -97,7 +98,8 @@ public class ArticleController {
             }
         }
 
-        List<Article> list = articleService.getFeatured(size, dimensionCode, userId, familyId, vendorType);
+        Long memberId = ParamUtils.getLong(body.get("memberId"), currentMemberId);
+        List<Article> list = articleService.getFeatured(size, dimensionCode, userId, familyId, vendorType, memberId);
         return Result.success(list);
     }
 

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

@@ -28,6 +28,8 @@ public class ArticleReadingRecord implements Serializable {
     // 关联的任务ID(阅读达标后自动创建的任务)
     private Long taskId;
 
+    private Boolean readingCompleted;
+
     private Date readAt;
 
     private Date updatedAt;

+ 7 - 0
cfc-backend/src/main/java/com/etotem/cfc/mapper/ArticleReadingRecordMapper.java

@@ -47,4 +47,11 @@ public interface ArticleReadingRecordMapper extends BaseMapper<ArticleReadingRec
 
     @Select("SELECT COUNT(DISTINCT child_id) FROM article_reading_records WHERE article_id = #{articleId}")
     long countDistinctByArticle(@Param("articleId") Long articleId);
+
+    /**
+     * 查询某用户已达标阅读的文章 ID 列表
+     */
+    @Select("SELECT article_id FROM article_reading_records " +
+            "WHERE child_id = #{childId} AND reading_completed = 1")
+    List<Long> selectCompletedArticleIdsByChild(@Param("childId") Long childId);
 }

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

@@ -130,11 +130,11 @@ public class ArticleService {
         return filtered;
     }
 
-    public List<Article> getFeatured(int size, Long userId, Long familyId, String vendorType) {
-        return getFeatured(size, null, userId, familyId, vendorType);
+    public List<Article> getFeatured(int size, Long userId, Long familyId, String vendorType, Long memberId) {
+        return getFeatured(size, null, userId, familyId, vendorType, memberId);
     }
 
-    public List<Article> getFeatured(int size, String dimensionCode, Long userId, Long familyId, String vendorType) {
+    public List<Article> getFeatured(int size, String dimensionCode, Long userId, Long familyId, String vendorType, Long memberId) {
         LambdaQueryWrapper<Article> wrapper = new LambdaQueryWrapper<Article>()
                 .eq(Article::getStatus, "published")
                 .eq(Article::getIsFeatured, 1)
@@ -154,6 +154,14 @@ public class ArticleService {
         List<Article> all = articleMapper.selectList(wrapper);
         List<Article> visible = articlePermissionService.filterVisible(all, userId, familyId, vendorType);
 
+        // 过滤已阅读达标的文章(达到阈值后不再出现在推荐阅读中)
+        if (memberId != null) {
+            List<Long> completedIds = articleReadingRecordMapper.selectCompletedArticleIdsByChild(memberId);
+            if (!completedIds.isEmpty()) {
+                visible.removeIf(a -> completedIds.contains(a.getId()));
+            }
+        }
+
         enrichTotalDurationSeconds(visible);
 
         return visible.size() > size ? visible.subList(0, size) : visible;
@@ -199,6 +207,8 @@ public class ArticleService {
         record.setChildId(memberId);
         record.setContent("");
         record.setDurationSeconds(durationSeconds);
+        int minSeconds = getArticleReadingMinSeconds();
+        record.setReadingCompleted(memberId != null && durationSeconds >= minSeconds);
         record.setReadAt(new Date());
         record.setCreatedAt(new Date());
         articleReadingRecordMapper.insert(record);
@@ -209,11 +219,7 @@ public class ArticleService {
         }
 
         // 4. Check reading duration threshold
-        int minSeconds = 60; // default
-        String minSecondsStr = sysConfigService.getValue("article_reading_min_seconds");
-        if (minSecondsStr != null && !minSecondsStr.isEmpty()) {
-            try { minSeconds = Integer.parseInt(minSecondsStr); } catch (NumberFormatException e) { /* use default */ }
-        }
+        int minSeconds = getArticleReadingMinSeconds();
 
         if (durationSeconds < minSeconds) {
             result.put("belowThreshold", true);
@@ -344,9 +350,18 @@ public class ArticleService {
                 .last("LIMIT 1");
         ArticleReadingRecord existing = articleReadingRecordMapper.selectOne(wrapper);
 
+        // 获取阅读达标阈值
+        int minSeconds = getArticleReadingMinSeconds();
+        boolean isCompleted = false;
+
         if (existing != null) {
             // 累加时长
             existing.setDurationSeconds(existing.getDurationSeconds() + durationSeconds);
+            // 累计时长达到阈值时标记为已阅读
+            if (existing.getReadingCompleted() == null || !existing.getReadingCompleted()) {
+                existing.setReadingCompleted(existing.getDurationSeconds() >= minSeconds);
+                isCompleted = existing.getReadingCompleted();
+            }
             existing.setUpdatedAt(new Date());
             articleReadingRecordMapper.updateById(existing);
         } else {
@@ -354,14 +369,26 @@ public class ArticleService {
             ArticleReadingRecord record = new ArticleReadingRecord();
             record.setArticleId(articleId);
             record.setUserId(userId);
-        record.setChildId(memberId);
+            record.setChildId(memberId);
             record.setDurationSeconds(durationSeconds);
+            record.setReadingCompleted(durationSeconds >= minSeconds);
             record.setReadAt(new Date());
             record.setCreatedAt(new Date());
             articleReadingRecordMapper.insert(record);
         }
     }
 
+    /**
+     * 获取文章阅读达标最低秒数,默认 60 秒
+     */
+    private int getArticleReadingMinSeconds() {
+        String val = sysConfigService.getValue("article_reading_min_seconds");
+        if (val != null && !val.isEmpty()) {
+            try { return Integer.parseInt(val); } catch (NumberFormatException e) {}
+        }
+        return 60;
+    }
+
     /**
      * 递增文章浏览数(打开详情页即计数,匿名/登录用户均计)
      */

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

@@ -4075,6 +4075,7 @@ CREATE TABLE IF NOT EXISTS article_reading_records (
   content VARCHAR(500) COMMENT '阅读内容摘要',
   duration_seconds INT DEFAULT 0 COMMENT '阅读时长(秒)',
   task_id BIGINT DEFAULT NULL COMMENT '关联的任务ID',
+  reading_completed TINYINT DEFAULT 0 COMMENT '是否已阅读达标(达到最低时长阈值)',
   read_at DATETIME COMMENT '阅读时间',
   updated_at DATETIME DEFAULT NULL COMMENT '更新时间',
   created_at DATETIME,

+ 1 - 1
cfc-web/.last_build_commit

@@ -1 +1 @@
-6d07887906f532406ec6b2ad9c203921b21a0ccd
+2485177021790e15ec396a0e7b6b2a9b384486c6

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

@@ -1,12 +1,12 @@
 {
   "name": "cfc-web",
-  "version": "1.0.1415",
+  "version": "1.0.1416",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "cfc-web",
-      "version": "1.0.1415",
+      "version": "1.0.1416",
       "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.1416",
+  "version": "1.0.1417",
   "private": true,
   "scripts": {
     "dev": "vue-cli-service serve",

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

@@ -4,6 +4,19 @@
 
 ---
 
+## v1.0.1417 (2026-09-15)
+
+### Bug 修复
+- meal_configs 唯一键改用生成列 config_date_eff 修复 COALESCE 索引语法
+
+### 其他
+- 迁移314 的 UNIQUE KEY uk_config (family_id, COALESCE(config_date, ...)) 每次启动都报语法错误,
+- 导致唯一约束从未建立, 存在重复共餐配置风险。
+- 改为 STORED 生成列 config_date_eff (COALESCE(config_date, '9999-12-31')) 承载相同语义,
+- 唯一键建立在生成列上, 同时补齐缺失的 idx_config_date 索引。
+- 
+
+
 ## v1.0.1416 (2026-09-15)
 
 ### Bug 修复

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

@@ -1,6 +1,6 @@
 # 更新日志
 
-> 当前版本: v1.0.1416
+> 当前版本: v1.0.1417
 
 ## 历史版本
 
@@ -8,6 +8,19 @@
 
 ---
 
+## v1.0.1417 (2026-09-15)
+
+### Bug 修复
+- meal_configs 唯一键改用生成列 config_date_eff 修复 COALESCE 索引语法
+
+### 其他
+- 迁移314 的 UNIQUE KEY uk_config (family_id, COALESCE(config_date, ...)) 每次启动都报语法错误,
+- 导致唯一约束从未建立, 存在重复共餐配置风险。
+- 改为 STORED 生成列 config_date_eff (COALESCE(config_date, '9999-12-31')) 承载相同语义,
+- 唯一键建立在生成列上, 同时补齐缺失的 idx_config_date 索引。
+- 
+
+
 ## v1.0.1416 (2026-09-15)
 
 ### Bug 修复