Sfoglia il codice sorgente

feat(challenge): ChallengeScoreService方向性评分结算引擎

asus 1 mese fa
parent
commit
756f9e11ba

+ 231 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/ChallengeScoreService.java

@@ -0,0 +1,231 @@
+package com.etotem.cfc.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.etotem.cfc.entity.FamilyChallengeScoreConfig;
+import com.etotem.cfc.entity.FamilyMember;
+import com.etotem.cfc.entity.FamilyRelationshipScore;
+import com.etotem.cfc.mapper.FamilyChallengeScoreConfigMapper;
+import com.etotem.cfc.mapper.FamilyMemberMapper;
+import com.etotem.cfc.mapper.FamilyRelationshipScoreMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.annotation.Resource;
+import java.math.BigDecimal;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 家庭挑战方向性关系评分结算引擎。
+ *
+ * 方向性语义:A→B 表示"A 对 B 的分数",存于 family_relationship_scores 行 (A→B)。
+ * 动作执行者(actor)永远是被加减的主体,落点在其"出向"行上;反向行不动。
+ * 数值从 family_challenge_score_config 读取(后台可配置),方向由本类代码固定。
+ */
+@Slf4j
+@Service
+public class ChallengeScoreService {
+
+    /** 配置默认值兜底(与 spec §3.4 一致):action -> scoreType -> value */
+    private static final Map<String, Map<String, Integer>> DEFAULT_CONFIG = new HashMap<>();
+
+    static {
+        Map<String, Integer> initiate = new HashMap<>();
+        initiate.put("energy", 1);
+        initiate.put("trust", 0);
+        initiate.put("intimacy", 1);
+        initiate.put("communication", 1);
+        DEFAULT_CONFIG.put("initiate", initiate);
+
+        Map<String, Integer> agree = new HashMap<>();
+        agree.put("energy", 1);
+        agree.put("trust", 1);
+        agree.put("intimacy", 1);
+        agree.put("communication", 1);
+        DEFAULT_CONFIG.put("agree", agree);
+
+        Map<String, Integer> reject = new HashMap<>();
+        reject.put("energy", -1);
+        reject.put("trust", -1);
+        reject.put("intimacy", -1);
+        reject.put("communication", 1); // 幅度,方向由代码固定
+        DEFAULT_CONFIG.put("reject", reject);
+    }
+
+    @Resource
+    private FamilyRelationshipScoreMapper relationshipScoreMapper;
+
+    @Resource
+    private FamilyChallengeScoreConfigMapper scoreConfigMapper;
+
+    @Resource
+    private FamilyMemberMapper familyMemberMapper;
+
+    @Resource
+    private EnergyService energyService;
+
+    /** 发起结算:I→每个其他参与成员 trust+0/intimacy+1/comm+1;能量仅发起人 +1 */
+    @Transactional
+    public void applyInitiate(Long familyId, Long initiatorId, List<Long> participantIds, Long challengeId) {
+        if (familyId == null || initiatorId == null || participantIds == null) return;
+        int intimacy = configValue("initiate", "intimacy");
+        int communication = configValue("initiate", "communication");
+        for (Long otherId : participantIds) {
+            if (otherId == null || otherId.equals(initiatorId)) continue;
+            adjustScore(familyId, initiatorId, otherId,
+                    configValue("initiate", "trust"), intimacy, communication);
+        }
+        awardEnergy(initiatorId, challengeId, configValue("initiate", "energy"), "发起家庭挑战");
+    }
+
+    /** 同意结算:X→每个其他参与成员 trust+1/intimacy+1/comm+1;能量 X 本人 +1 */
+    @Transactional
+    public void applyAgree(Long familyId, Long memberId, List<Long> participantIds, Long challengeId) {
+        if (familyId == null || memberId == null || participantIds == null) return;
+        int trust = configValue("agree", "trust");
+        int intimacy = configValue("agree", "intimacy");
+        int communication = configValue("agree", "communication");
+        for (Long otherId : participantIds) {
+            if (otherId == null || otherId.equals(memberId)) continue;
+            adjustScore(familyId, memberId, otherId, trust, intimacy, communication);
+        }
+        awardEnergy(memberId, challengeId, configValue("agree", "energy"), "同意家庭挑战");
+    }
+
+    /**
+     * 拒绝结算(矩阵见类注释):
+     * I→X: trust-1/intimacy-1/comm-1;X→I: 仅 comm+1;已同意成员A→X: 仅 comm-1(C3);能量 I+A 每人 -1
+     */
+    @Transactional
+    public void applyReject(Long familyId, Long rejectorId, Long initiatorId, List<Long> agreedMemberIds, Long challengeId) {
+        if (familyId == null || rejectorId == null || initiatorId == null) return;
+        int commAbs = Math.abs(configValue("reject", "communication"));
+
+        // I→X: trust/intimacy/communication 全扣
+        adjustScore(familyId, initiatorId, rejectorId,
+                configValue("reject", "trust"),
+                configValue("reject", "intimacy"),
+                -commAbs);
+
+        // X→I: 仅 communication +1(trust/intimacy 不动)
+        adjustScore(familyId, rejectorId, initiatorId, 0, 0, commAbs);
+
+        // 已同意成员 A→X: 仅 communication -1(C3,trust/intimacy 不扣)
+        if (agreedMemberIds != null) {
+            for (Long a : agreedMemberIds) {
+                if (a == null || a.equals(rejectorId)) continue;
+                adjustScore(familyId, a, rejectorId, 0, 0, -commAbs);
+            }
+        }
+
+        // 能量:发起人 + 已同意成员 每人 -1
+        int energyVal = configValue("reject", "energy");
+        deductEnergy(initiatorId, challengeId, energyVal, "成员拒绝家庭挑战");
+        if (agreedMemberIds != null) {
+            for (Long a : agreedMemberIds) {
+                if (a == null || a.equals(rejectorId)) continue;
+                deductEnergy(a, challengeId, energyVal, "成员拒绝家庭挑战");
+            }
+        }
+    }
+
+    // ==================== 私有辅助 ====================
+
+    /** 读取配置值;无配置行/禁用时用内置默认 */
+    private int configValue(String action, String scoreType) {
+        try {
+            FamilyChallengeScoreConfig cfg = scoreConfigMapper.selectOne(
+                    new LambdaQueryWrapper<FamilyChallengeScoreConfig>()
+                            .eq(FamilyChallengeScoreConfig::getAction, action)
+                            .eq(FamilyChallengeScoreConfig::getScoreType, scoreType));
+            if (cfg != null && cfg.getValue() != null && (cfg.getEnabled() == null || cfg.getEnabled() == 1)) {
+                return cfg.getValue();
+            }
+        } catch (Exception e) {
+            log.warn("查询挑战评分配置失败: action={}, scoreType={}, err={}", action, scoreType, e.getMessage());
+        }
+        Map<String, Integer> actionCfg = DEFAULT_CONFIG.get(action);
+        Integer v = actionCfg != null ? actionCfg.get(scoreType) : null;
+        return v != null ? v : 0;
+    }
+
+    /** 对 (from→to) 方向行增减三项分数(delta 为 0 时跳过该列) */
+    private void adjustScore(Long familyId, Long fromId, Long toId,
+                             int trustDelta, int intimacyDelta, int communicationDelta) {
+        FamilyRelationshipScore score = getOrCreateScore(familyId, fromId, toId);
+        if (trustDelta != 0) {
+            score.setTrustScore(safeAdd(score.getTrustScore(), trustDelta));
+        }
+        if (intimacyDelta != 0) {
+            score.setIntimacyScore(safeAdd(score.getIntimacyScore(), intimacyDelta));
+        }
+        if (communicationDelta != 0) {
+            score.setCommunicationScore(safeAdd(score.getCommunicationScore(), communicationDelta));
+        }
+        relationshipScoreMapper.updateById(score);
+    }
+
+    /**
+     * 获取 (from→to) 方向行;不存在则创建。
+     * 初始基线:取 from 成员在 family_members 的成员级分数(无方向),缺失默认 60.00。
+     * 并发兜底:insert 撞唯一键时重查返回已有行。
+     */
+    private FamilyRelationshipScore getOrCreateScore(Long familyId, Long fromId, Long toId) {
+        FamilyRelationshipScore existing = relationshipScoreMapper.selectOne(
+                new LambdaQueryWrapper<FamilyRelationshipScore>()
+                        .eq(FamilyRelationshipScore::getFromMemberId, fromId)
+                        .eq(FamilyRelationshipScore::getToMemberId, toId));
+        if (existing != null) return existing;
+
+        FamilyRelationshipScore score = new FamilyRelationshipScore();
+        score.setFamilyId(familyId);
+        score.setFromMemberId(fromId);
+        score.setToMemberId(toId);
+        FamilyMember from = familyMemberMapper.selectById(fromId);
+        score.setTrustScore(from != null && from.getTrustScore() != null
+                ? from.getTrustScore() : new BigDecimal("60.00"));
+        score.setIntimacyScore(from != null && from.getIntimacyScore() != null
+                ? from.getIntimacyScore() : new BigDecimal("60.00"));
+        score.setCommunicationScore(from != null && from.getCommunicationScore() != null
+                ? from.getCommunicationScore() : new BigDecimal("60.00"));
+        try {
+            relationshipScoreMapper.insert(score);
+            return score;
+        } catch (Exception e) {
+            FamilyRelationshipScore retry = relationshipScoreMapper.selectOne(
+                    new LambdaQueryWrapper<FamilyRelationshipScore>()
+                            .eq(FamilyRelationshipScore::getFromMemberId, fromId)
+                            .eq(FamilyRelationshipScore::getToMemberId, toId));
+            if (retry != null) return retry;
+            throw e;
+        }
+    }
+
+    private BigDecimal safeAdd(BigDecimal base, int delta) {
+        BigDecimal v = base != null ? base : BigDecimal.ZERO;
+        return v.add(BigDecimal.valueOf(delta));
+    }
+
+    private void awardEnergy(Long memberId, Long challengeId, int amount, String reason) {
+        if (amount <= 0) return;
+        try {
+            energyService.awardEnergyByCode(memberId, "action", amount, reason + "(挑战ID:" + challengeId + ")");
+        } catch (Exception e) {
+            log.error("挑战能量发放失败: memberId={}, challengeId={}", memberId, challengeId, e);
+        }
+    }
+
+    private void deductEnergy(Long memberId, Long challengeId, int amount, String reason) {
+        if (amount >= 0) return;
+        try {
+            int result = energyService.deductEnergyByCode(memberId, "action", -amount, reason + "(挑战ID:" + challengeId + ")");
+            if (result == -1) {
+                log.warn("挑战能量扣减跳过(余额不足): memberId={}, challengeId={}", memberId, challengeId);
+            }
+        } catch (Exception e) {
+            log.error("挑战能量扣减失败: memberId={}, challengeId={}", memberId, challengeId, e);
+        }
+    }
+}