|
|
@@ -0,0 +1,129 @@
|
|
|
+package com.etotem.cfc.service;
|
|
|
+
|
|
|
+import com.alibaba.fastjson.JSON;
|
|
|
+import com.alibaba.fastjson.JSONArray;
|
|
|
+import com.alibaba.fastjson.JSONObject;
|
|
|
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
+import com.etotem.cfc.entity.ContentDimensionThreshold;
|
|
|
+import com.etotem.cfc.util.DimensionCodes;
|
|
|
+import org.springframework.stereotype.Component;
|
|
|
+
|
|
|
+import java.util.*;
|
|
|
+import java.util.function.Function;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 内容维度阈值过滤工具。
|
|
|
+ *
|
|
|
+ * <p>显隐规则(OR 逻辑):对某个内容项,若其配置的任一维度上「用户指标值 < 阈值」→ 该内容显示;
|
|
|
+ * 若其配置的全部维度上「用户指标值 ≥ 阈值」→ 该内容不显示(隐藏)。
|
|
|
+ * 「小于阈值显示,大于或等于阈值隐藏」。
|
|
|
+ *
|
|
|
+ * <p>Fail-open:
|
|
|
+ * <ul>
|
|
|
+ * <li>无阈值配置 → 保留</li>
|
|
|
+ * <li>无得分数据(familyAvgScores 取不到对应 key)→ 视为无分,按「显示」处理</li>
|
|
|
+ * <li>familyAvgScores / thresholdsByContentId 为 null/空 → 全部保留</li>
|
|
|
+ * </ul>
|
|
|
+ *
|
|
|
+ * <p>使用方式(内存循环,在 service 层 selectPage 之后、返回 DTO 之前调用):
|
|
|
+ * <pre>{@code
|
|
|
+ * Map<String, Integer> familyAvg = fiveDimensionScoreService.getFamilyAverageScores(familyId);
|
|
|
+ * List<Long> ids = records.stream().map(Item::getId).collect(toList());
|
|
|
+ * Map<Long, List<ContentDimensionThreshold>> byId = thresholdService.listByContents(contentType, ids);
|
|
|
+ * DimensionThresholdFilter.filterVisible(records, Item::getId, contentType, familyAvg, byId);
|
|
|
+ * }</pre>
|
|
|
+ */
|
|
|
+@Component
|
|
|
+public class DimensionThresholdFilter {
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 对 items 就地移除满足「全部维度得分 >= 阈值」的内容项(即隐藏)。
|
|
|
+ *
|
|
|
+ * @param items 待过滤列表(in-place 修改)
|
|
|
+ * @param idGetter 从 item 提取 contentId
|
|
|
+ * @param contentType 内容类型 PRODUCT|ARTICLE|ACTIVITY|MINIGAME
|
|
|
+ * @param familyAvgScores 家庭平均分 Map<dimensionCode, score>(key 含五维码/子项码/自定义码)
|
|
|
+ * @param thresholdsByContentId contentId → 该内容的阈值列表
|
|
|
+ */
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
+ public static <T> void filterVisible(List<T> items, Function<T, Long> idGetter,
|
|
|
+ String contentType,
|
|
|
+ Map<String, Integer> familyAvgScores,
|
|
|
+ Map<Long, List<ContentDimensionThreshold>> thresholdsByContentId) {
|
|
|
+ if (items == null || items.isEmpty()) return;
|
|
|
+ if (familyAvgScores == null) {
|
|
|
+ // 无得分数据 → fail-open 全部保留
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (thresholdsByContentId == null || thresholdsByContentId.isEmpty()) {
|
|
|
+ // 无阈值配置 → fail-open 全部保留
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ items.removeIf(item -> {
|
|
|
+ Long id = idGetter.apply(item);
|
|
|
+ if (id == null) return false;
|
|
|
+ List<ContentDimensionThreshold> ths = thresholdsByContentId.get(id);
|
|
|
+ if (ths == null || ths.isEmpty()) return false; // 无阈值 → 保留
|
|
|
+ return shouldHide(ths, familyAvgScores);
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 全部维度得分 ≥ 阈值 → 返回 true(隐藏);任一维度得分 < 阈值 → 返回 false(保留)。
|
|
|
+ * 任何维度得分缺失 → fail-open,视为「显示」。
|
|
|
+ */
|
|
|
+ private static boolean shouldHide(List<ContentDimensionThreshold> ths, Map<String, Integer> scores) {
|
|
|
+ for (ContentDimensionThreshold t : ths) {
|
|
|
+ if (t == null || t.getDimensionCode() == null || t.getThreshold() == null) continue;
|
|
|
+ Integer v = scores.get(t.getDimensionCode());
|
|
|
+ if (v == null) return false; // 无分 → fail-open
|
|
|
+ if (v < t.getThreshold()) return false; // 任一 < 阈值 → 保留
|
|
|
+ }
|
|
|
+ return true; // 全部 >= 阈值 → 隐藏
|
|
|
+ }
|
|
|
+
|
|
|
+ // ---- detailJson 子项展开工具(供 FiveDimensionScoreService 调用) ----
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 把 FiveDimensionScore.detailJson 中所有「非五维码」的数值叶子字段展开到 code→值映射。
|
|
|
+ * 支持顶层平铺 JSON 与嵌套 JSON 对象/数组混合形态。
|
|
|
+ * 遇到已收录的 (memberId, code) 只保留首次(最新)一条。
|
|
|
+ *
|
|
|
+ * @param detailJson detail_json 字段字符串
|
|
|
+ * @param memberId 成员 ID
|
|
|
+ * @param codeToMemberScore code → (memberId → score) 累积 Map(直接修改)
|
|
|
+ */
|
|
|
+ public static void expandDetailJson(String detailJson, Long memberId,
|
|
|
+ Map<String, Map<Long, Integer>> codeToMemberScore) {
|
|
|
+ if (detailJson == null || detailJson.isEmpty()) return;
|
|
|
+ try {
|
|
|
+ Object root = JSON.parse(detailJson);
|
|
|
+ collectNumericLeaves(root, memberId, codeToMemberScore);
|
|
|
+ } catch (Exception ignored) {
|
|
|
+ // 非法 JSON 静默忽略,避免影响主逻辑
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private static void collectNumericLeaves(Object node, Long memberId,
|
|
|
+ Map<String, Map<Long, Integer>> codeToMemberScore) {
|
|
|
+ if (node == null) return;
|
|
|
+ if (node instanceof JSONObject) {
|
|
|
+ JSONObject obj = (JSONObject) node;
|
|
|
+ for (String key : obj.keySet()) {
|
|
|
+ if (DimensionCodes.isValid(key)) continue; // 五维码跳过(已在主记录聚合)
|
|
|
+ Object val = obj.get(key);
|
|
|
+ if (val instanceof Number) {
|
|
|
+ codeToMemberScore.computeIfAbsent(key, k -> new HashMap<>())
|
|
|
+ .putIfAbsent(memberId, ((Number) val).intValue());
|
|
|
+ } else {
|
|
|
+ collectNumericLeaves(val, memberId, codeToMemberScore);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else if (node instanceof JSONArray) {
|
|
|
+ JSONArray arr = (JSONArray) node;
|
|
|
+ for (int i = 0; i < arr.size(); i++) {
|
|
|
+ collectNumericLeaves(arr.get(i), memberId, codeToMemberScore);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|