Ver Fonte

feat(api): 改版v1 会员任务标记(B20-22) + 问卷过滤(B23) + 内容标签过滤(B24)

E2E Test Bot há 1 mês atrás
pai
commit
1e492785da

+ 160 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/ContentFilterController.java

@@ -0,0 +1,160 @@
+package com.etotem.cfc.controller;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.Article;
+import com.etotem.cfc.entity.Food;
+import com.etotem.cfc.entity.HealthKnowledgeBase;
+import com.etotem.cfc.entity.ProblemDomain;
+import com.etotem.cfc.mapper.ArticleMapper;
+import com.etotem.cfc.mapper.FoodMapper;
+import com.etotem.cfc.mapper.HealthKnowledgeBaseMapper;
+import com.etotem.cfc.mapper.ProblemDomainMapper;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import javax.annotation.Resource;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * 内容过滤 — 改版v1(B24)
+ * <p>
+ * 用 resultTags + 问题域 code 过滤文章/知识库/饮食推荐。
+ */
+@Tag(name = "内容过滤", description = "按 resultTags + 问题域过滤文章/知识库/饮食")
+@RestController
+@RequestMapping("/api/content")
+public class ContentFilterController {
+
+    @Resource
+    private ArticleMapper articleMapper;
+
+    @Resource
+    private HealthKnowledgeBaseMapper healthKnowledgeBaseMapper;
+
+    @Resource
+    private FoodMapper foodMapper;
+
+    @Resource
+    private ProblemDomainMapper problemDomainMapper;
+
+    @Operation(summary = "按 resultTags + 问题域 code 过滤文章/知识库/饮食")
+    @PostMapping("/by-tags")
+    public Result<Map<String, Object>> byTags(@RequestBody Map<String, Object> params) {
+        Set<String> tags = new LinkedHashSet<>();
+        Object resultTagsObj = params.get("resultTags");
+        if (resultTagsObj instanceof List) {
+            for (Object t : (List<?>) resultTagsObj) {
+                if (t != null && !t.toString().trim().isEmpty()) {
+                    tags.add(t.toString().trim());
+                }
+            }
+        }
+        Object problemDomainCode = params.get("problemDomainCode");
+        if (problemDomainCode != null && !problemDomainCode.toString().isEmpty()) {
+            ProblemDomain domain = problemDomainMapper.selectOne(
+                    new LambdaQueryWrapper<ProblemDomain>()
+                            .eq(ProblemDomain::getCode, problemDomainCode.toString())
+                            .last("LIMIT 1"));
+            if (domain != null && domain.getContentTags() != null) {
+                for (String tag : domain.getContentTags().split(",")) {
+                    if (!tag.trim().isEmpty()) {
+                        tags.add(tag.trim());
+                    }
+                }
+            }
+        }
+        int size = params.get("size") != null ? Integer.valueOf(params.get("size").toString()) : 10;
+
+        Map<String, Object> data = new HashMap<>();
+        data.put("resultTags", new ArrayList<>(tags));
+        data.put("articles", filterArticles(tags, size));
+        data.put("knowledgeBases", filterKnowledge(tags, size));
+        data.put("foods", filterFoods(tags, size));
+        return Result.success(data);
+    }
+
+    /** 文章:tags 字段 LIKE 任一标签 */
+    private List<Article> filterArticles(Set<String> tags, int size) {
+        if (tags.isEmpty()) {
+            return new ArrayList<>();
+        }
+        LambdaQueryWrapper<Article> wrapper = new LambdaQueryWrapper<Article>()
+                .eq(Article::getStatus, "published")
+                .orderByDesc(Article::getIsFeatured)
+                .orderByDesc(Article::getPublishedAt)
+                .last("LIMIT " + size);
+        wrapper.and(w -> {
+            boolean first = true;
+            for (String tag : tags) {
+                if (first) {
+                    w.like(Article::getTags, tag);
+                    first = false;
+                } else {
+                    w.or().like(Article::getTags, tag);
+                }
+            }
+        });
+        return articleMapper.selectList(wrapper);
+    }
+
+    /** 知识库:itemName/category/description LIKE 任一标签 */
+    private List<HealthKnowledgeBase> filterKnowledge(Set<String> tags, int size) {
+        if (tags.isEmpty()) {
+            return new ArrayList<>();
+        }
+        LambdaQueryWrapper<HealthKnowledgeBase> wrapper = new LambdaQueryWrapper<HealthKnowledgeBase>()
+                .orderByAsc(HealthKnowledgeBase::getId)
+                .last("LIMIT " + size);
+        wrapper.and(w -> {
+            boolean first = true;
+            for (String tag : tags) {
+                if (first) {
+                    w.like(HealthKnowledgeBase::getItemName, tag)
+                            .or().like(HealthKnowledgeBase::getCategory, tag);
+                    first = false;
+                } else {
+                    w.or().like(HealthKnowledgeBase::getItemName, tag)
+                            .or().like(HealthKnowledgeBase::getCategory, tag);
+                }
+            }
+        });
+        return healthKnowledgeBaseMapper.selectList(wrapper);
+    }
+
+    /** 饮食:nutritionTags/suitableFor/name LIKE 任一标签 */
+    private List<Food> filterFoods(Set<String> tags, int size) {
+        if (tags.isEmpty()) {
+            return new ArrayList<>();
+        }
+        LambdaQueryWrapper<Food> wrapper = new LambdaQueryWrapper<Food>()
+                .orderByAsc(Food::getSortOrder)
+                .orderByAsc(Food::getId)
+                .last("LIMIT " + size);
+        wrapper.and(w -> {
+            boolean first = true;
+            for (String tag : tags) {
+                if (first) {
+                    w.like(Food::getNutritionTags, tag)
+                            .or().like(Food::getSuitableFor, tag)
+                            .or().like(Food::getName, tag);
+                    first = false;
+                } else {
+                    w.or().like(Food::getNutritionTags, tag)
+                            .or().like(Food::getSuitableFor, tag)
+                            .or().like(Food::getName, tag);
+                }
+            }
+        });
+        return foodMapper.selectList(wrapper);
+    }
+}

+ 3 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/CreateTaskDTO.java

@@ -50,4 +50,7 @@ public class CreateTaskDTO {
     private String dimensionCode;
     // 五维权重JSON: {"body":0,"mind":0,"wisdom":0,"action":0,"wealth":0}
     private String dimensionWeights;
+
+    // 会员任务标记: 1=会员专属 0=普通(B20,未缴费用户自建任务固定为0)
+    private Integer memberOnly;
 }

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

@@ -65,6 +65,7 @@ public class PlanActivationService {
         task.setSourceId(plan.getId());
         task.setNeedReview(0);
         task.setIsTemplate(0);
+        task.setMemberOnly(1); // B20: 方案/套餐生成的任务为会员专属
         Calendar cal = Calendar.getInstance();
         cal.set(Calendar.HOUR_OF_DAY, 23);
         cal.set(Calendar.MINUTE, 59);

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

@@ -10,6 +10,7 @@ public interface SurveyService {
     List<SurveyTemplate> listTemplates(Integer enabled);
     void deleteTemplate(Long id, Long adminId);
     List<SurveyTemplate> getActiveTemplates();
+    List<SurveyTemplate> getActiveTemplatesByDimension(String dimension);
     Object getSurveyStatus(Long userId, Long memberId);
     SurveyRecord submitSurvey(Long userId, Long memberId, Long templateId, String answersJson, Date periodStart, Date periodEnd);
     List<SurveyRecord> getSurveyHistory(Long userId, Long memberId);

+ 3 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/TaskService.java

@@ -238,6 +238,9 @@ task.setReviewByCategory(dto.getReviewByCategory() != null ? dto.getReviewByCate
         task.setDimensionCode(dto.getDimensionCode());
         task.setDimensionWeights(dto.getDimensionWeights());
 
+        // B20: 会员任务标记透传(自建任务 memberOnly=0,不校验会员)
+        task.setMemberOnly(dto.getMemberOnly() != null ? dto.getMemberOnly() : 0);
+
         return task;
     }
 

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

@@ -79,6 +79,21 @@ public class SurveyServiceImpl implements SurveyService {
         return listTemplates(1);
     }
 
+    @Override
+    public List<SurveyTemplate> getActiveTemplatesByDimension(String dimension) {
+        List<SurveyTemplate> all = listTemplates(1);
+        if (dimension == null || dimension.trim().isEmpty()) {
+            return all;
+        }
+        List<SurveyTemplate> result = new ArrayList<>();
+        for (SurveyTemplate t : all) {
+            if (dimension.equalsIgnoreCase(t.getDimension())) {
+                result.add(t);
+            }
+        }
+        return result;
+    }
+
     @Override
     public Object getSurveyStatus(Long userId, Long memberId) {
         memberId = resolveMemberId(userId, memberId);

+ 1 - 0
cfc-backend/src/main/java/com/etotem/cfc/task/RepeatTaskGenerator.java

@@ -139,6 +139,7 @@ newTask.setBonusRule(template.getBonusRule());
 newTask.setPointsExpireDays(template.getPointsExpireDays());
 newTask.setTemplateId(template.getId()); // 指向模板
 newTask.setIsTemplate(0); // 这是实例,不是模板
+newTask.setMemberOnly(template.getMemberOnly()); // B21: 透传会员标记
 
 // 设置新的截止时间(保持原有时分,更新日期)
 Calendar newDeadline = Calendar.getInstance();