|
@@ -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);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|