|
@@ -6,6 +6,9 @@ import com.etotem.cfc.dto.EnergySandboxDTO;
|
|
|
import com.etotem.cfc.dto.MemberEnergyDTO;
|
|
import com.etotem.cfc.dto.MemberEnergyDTO;
|
|
|
import com.etotem.cfc.entity.*;
|
|
import com.etotem.cfc.entity.*;
|
|
|
import com.etotem.cfc.mapper.*;
|
|
import com.etotem.cfc.mapper.*;
|
|
|
|
|
+import com.alibaba.fastjson.JSON;
|
|
|
|
|
+import com.alibaba.fastjson.JSONArray;
|
|
|
|
|
+import com.alibaba.fastjson.JSONObject;
|
|
|
import lombok.extern.slf4j.Slf4j;
|
|
import lombok.extern.slf4j.Slf4j;
|
|
|
import org.springframework.stereotype.Service;
|
|
import org.springframework.stereotype.Service;
|
|
|
import org.springframework.transaction.annotation.Transactional;
|
|
import org.springframework.transaction.annotation.Transactional;
|
|
@@ -1096,6 +1099,9 @@ public class EnergyService {
|
|
|
@Resource
|
|
@Resource
|
|
|
private EnergySourceConfigMapper energySourceConfigMapper;
|
|
private EnergySourceConfigMapper energySourceConfigMapper;
|
|
|
|
|
|
|
|
|
|
+ @Resource
|
|
|
|
|
+ private EnergyBehaviorConfigMapper energyBehaviorConfigMapper;
|
|
|
|
|
+
|
|
|
@Resource
|
|
@Resource
|
|
|
private EnergyLogMapper energyLogMapper;
|
|
private EnergyLogMapper energyLogMapper;
|
|
|
|
|
|
|
@@ -1127,7 +1133,13 @@ public class EnergyService {
|
|
|
Integer daysToExpire) {
|
|
Integer daysToExpire) {
|
|
|
if (totalAmount == null || totalAmount <= 0) return new HashMap<>();
|
|
if (totalAmount == null || totalAmount <= 0) return new HashMap<>();
|
|
|
|
|
|
|
|
- // 1. 查比例配置 → Fallback链
|
|
|
|
|
|
|
+ // 0. 查 behavior_config:sourceType 映射到 behavior_code
|
|
|
|
|
+ EnergyBehaviorConfig behaviorConfig = matchBehaviorConfig(sourceType);
|
|
|
|
|
+ if (behaviorConfig != null) {
|
|
|
|
|
+ return awardByConfig(childId, behaviorConfig, sourceType, sourceId, totalAmount, description, daysToExpire);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 1. 无 behavior_config 时使用旧逻辑:查 energy_source_config → Fallback
|
|
|
List<EnergySourceConfig> configs = energySourceConfigMapper.selectList(
|
|
List<EnergySourceConfig> configs = energySourceConfigMapper.selectList(
|
|
|
new LambdaQueryWrapper<EnergySourceConfig>()
|
|
new LambdaQueryWrapper<EnergySourceConfig>()
|
|
|
.eq(EnergySourceConfig::getSourceType, sourceType)
|
|
.eq(EnergySourceConfig::getSourceType, sourceType)
|
|
@@ -1143,14 +1155,64 @@ public class EnergyService {
|
|
|
return new HashMap<>();
|
|
return new HashMap<>();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 3. 按比例分配(整数,余数加到最大比例维度)
|
|
|
|
|
|
|
+ // 3. 按比例分配
|
|
|
Map<Long, Integer> allocations = calculateAllocations(totalAmount, dimRatios);
|
|
Map<Long, Integer> allocations = calculateAllocations(totalAmount, dimRatios);
|
|
|
-
|
|
|
|
|
// 4. 更新余额 + 写流水
|
|
// 4. 更新余额 + 写流水
|
|
|
|
|
+ return executeAward(childId, sourceType, sourceId, description, daysToExpire, allocations);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 根据 behavior_config 发放能量
|
|
|
|
|
+ */
|
|
|
|
|
+ @Transactional
|
|
|
|
|
+ public Map<String, Integer> awardByConfig(Long childId, EnergyBehaviorConfig config,
|
|
|
|
|
+ String sourceType, Long sourceId,
|
|
|
|
|
+ Integer totalAmount, String description,
|
|
|
|
|
+ Integer daysToExpire) {
|
|
|
|
|
+ if (!isEnabled(config)) return new HashMap<>();
|
|
|
|
|
+
|
|
|
|
|
+ // 解析维度分配JSON
|
|
|
|
|
+ Map<Long, Integer> dimRatios = parseDimensionAssign(config.getDimensionAssign());
|
|
|
|
|
+ if (dimRatios.isEmpty()) {
|
|
|
|
|
+ // 无维度配置时使用旧 Fallback
|
|
|
|
|
+ EnergyDimension action = getDimByCode("action");
|
|
|
|
|
+ if (action != null) dimRatios = Collections.singletonMap(action.getId(), 10000);
|
|
|
|
|
+ else return new HashMap<>();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 日上限检查(使用配置的 daily_limit,大于0时覆盖全局限制)
|
|
|
|
|
+ Integer configDailyLimit = config.getDailyLimit();
|
|
|
|
|
+ if (configDailyLimit != null && configDailyLimit > 0) {
|
|
|
|
|
+ if (isDailyLimitExceeded(childId, dimRatios.keySet(), totalAmount, configDailyLimit)) {
|
|
|
|
|
+ log.warn("行为日上限已达,跳过: behaviorCode={}, childId={}", config.getBehaviorCode(), childId);
|
|
|
|
|
+ return new HashMap<>();
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ if (isDailyLimitExceeded(childId, dimRatios.keySet(), totalAmount)) {
|
|
|
|
|
+ log.warn("全局日上限已达,跳过: behaviorCode={}, childId={}", config.getBehaviorCode(), childId);
|
|
|
|
|
+ return new HashMap<>();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 计算过期时间
|
|
|
|
|
+ Integer expireDays = config.getExpireDays() != null && config.getExpireDays() > 0
|
|
|
|
|
+ ? config.getExpireDays() : (daysToExpire != null ? daysToExpire : null);
|
|
|
|
|
+
|
|
|
|
|
+ // 按比例分配
|
|
|
|
|
+ Map<Long, Integer> allocations = calculateAllocations(totalAmount, dimRatios);
|
|
|
|
|
+ return executeAward(childId, sourceType, sourceId, totalAmount, description, expireDays, allocations);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 执行能量发放(更新余额+写流水)
|
|
|
|
|
+ */
|
|
|
|
|
+ private Map<String, Integer> executeAward(Long childId, String sourceType, Long sourceId,
|
|
|
|
|
+ Integer totalAmount, String description,
|
|
|
|
|
+ Integer expireDays, Map<Long, Integer> allocations) {
|
|
|
Map<String, Integer> result = new LinkedHashMap<>();
|
|
Map<String, Integer> result = new LinkedHashMap<>();
|
|
|
Date now = new Date();
|
|
Date now = new Date();
|
|
|
- Date expiresAt = daysToExpire != null
|
|
|
|
|
- ? new Date(now.getTime() + (long) daysToExpire * 86400000L) : null;
|
|
|
|
|
|
|
+ Date expiresAt = expireDays != null
|
|
|
|
|
+ ? new Date(now.getTime() + (long) expireDays * 86400000L) : null;
|
|
|
|
|
|
|
|
for (Map.Entry<Long, Integer> entry : allocations.entrySet()) {
|
|
for (Map.Entry<Long, Integer> entry : allocations.entrySet()) {
|
|
|
if (entry.getValue() <= 0) continue;
|
|
if (entry.getValue() <= 0) continue;
|
|
@@ -1179,6 +1241,80 @@ public class EnergyService {
|
|
|
return result;
|
|
return result;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 通过 sourceType 匹配 behavior_config
|
|
|
|
|
+ * 映射规则: sourceType → behavior_code
|
|
|
|
|
+ * 如 "task" → "task_complete", "game" → "game_finish"
|
|
|
|
|
+ */
|
|
|
|
|
+ private EnergyBehaviorConfig matchBehaviorConfig(String sourceType) {
|
|
|
|
|
+ if (sourceType == null || sourceType.isEmpty()) return null;
|
|
|
|
|
+ String behaviorCode = sourceTypeToBehaviorCode(sourceType);
|
|
|
|
|
+ if (behaviorCode == null) return null;
|
|
|
|
|
+ try {
|
|
|
|
|
+ return energyBehaviorConfigMapper.selectOne(
|
|
|
|
|
+ new LambdaQueryWrapper<EnergyBehaviorConfig>()
|
|
|
|
|
+ .eq(EnergyBehaviorConfig::getBehaviorCode, behaviorCode)
|
|
|
|
|
+ .eq(EnergyBehaviorConfig::getEnabled, 1)
|
|
|
|
|
+ );
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.warn("查询能量行为配置失败: behaviorCode={}, error={}", behaviorCode, e.getMessage());
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * sourceType → behavior_code 映射
|
|
|
|
|
+ */
|
|
|
|
|
+ private String sourceTypeToBehaviorCode(String sourceType) {
|
|
|
|
|
+ switch (sourceType) {
|
|
|
|
|
+ case "task": return "task_complete";
|
|
|
|
|
+ case "game": return "game_finish";
|
|
|
|
|
+ case "activity": return "activity_checkin";
|
|
|
|
|
+ case "article": return "article_read";
|
|
|
|
|
+ case "product": return "product_purchase";
|
|
|
|
|
+ case "health_checkin": return "health_checkin";
|
|
|
|
|
+ case "checkin": return "finance_checkin"; // 理财打卡
|
|
|
|
|
+ case "emotion_checkin": return "emotion_checkin";
|
|
|
|
|
+ case "appointment": return "appointment_submit"; // 预约/完成共用
|
|
|
|
|
+ case "streak": return "streak_daily";
|
|
|
|
|
+ case "growth_day": return "growth_task";
|
|
|
|
|
+ case "micro_action": return "micro_action";
|
|
|
|
|
+ case "onboarding": return "onboarding";
|
|
|
|
|
+ case "invite_milestone": return "invite_milestone";
|
|
|
|
|
+ case "dimension": return "dimension_sync";
|
|
|
|
|
+ default: return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 解析维度分配JSON → Map<dimensionId, ratio>
|
|
|
|
|
+ * 输入: [{"dim":"action","ratio":50},{"dim":"wisdom","ratio":50}]
|
|
|
|
|
+ * 输出: {4: 5000, 3: 5000}
|
|
|
|
|
+ */
|
|
|
|
|
+ private Map<Long, Integer> parseDimensionAssign(String dimensionAssign) {
|
|
|
|
|
+ Map<Long, Integer> result = new LinkedHashMap<>();
|
|
|
|
|
+ if (dimensionAssign == null || dimensionAssign.isEmpty()) return result;
|
|
|
|
|
+ try {
|
|
|
|
|
+ com.alibaba.fastjson.JSONArray arr = com.alibaba.fastjson.JSON.parseArray(dimensionAssign);
|
|
|
|
|
+ for (int i = 0; i < arr.size(); i++) {
|
|
|
|
|
+ com.alibaba.fastjson.JSONObject item = arr.getJSONObject(i);
|
|
|
|
|
+ String dimCode = item.getString("dim");
|
|
|
|
|
+ int ratio = item.getInt("ratio");
|
|
|
|
|
+ EnergyDimension dim = getDimByCode(dimCode);
|
|
|
|
|
+ if (dim != null) {
|
|
|
|
|
+ result.put(dim.getId(), ratio * 100); // 转为10000制
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.warn("解析维度分配JSON失败: {}", e.getMessage());
|
|
|
|
|
+ }
|
|
|
|
|
+ return result;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private boolean isEnabled(EnergyBehaviorConfig config) {
|
|
|
|
|
+ return config != null && config.getEnabled() != null && config.getEnabled() == 1;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* 扣除能量
|
|
* 扣除能量
|
|
|
*
|
|
*
|
|
@@ -1448,12 +1584,19 @@ public class EnergyService {
|
|
|
* 检查日上限是否已达
|
|
* 检查日上限是否已达
|
|
|
*/
|
|
*/
|
|
|
private boolean isDailyLimitExceeded(Long childId, Set<Long> dimIds, Integer amount) {
|
|
private boolean isDailyLimitExceeded(Long childId, Set<Long> dimIds, Integer amount) {
|
|
|
|
|
+ return isDailyLimitExceeded(childId, dimIds, amount, GLOBAL_DAILY_LIMIT);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 检查日上限(支持自定义全局上限)
|
|
|
|
|
+ */
|
|
|
|
|
+ private boolean isDailyLimitExceeded(Long childId, Set<Long> dimIds, Integer amount, Integer customGlobalLimit) {
|
|
|
if (amount == null || amount <= 0) return true;
|
|
if (amount == null || amount <= 0) return true;
|
|
|
|
|
|
|
|
Date todayStart = getTodayStart();
|
|
Date todayStart = getTodayStart();
|
|
|
Date todayEnd = getTodayEnd();
|
|
Date todayEnd = getTodayEnd();
|
|
|
|
|
|
|
|
- // 全局日上限
|
|
|
|
|
|
|
+ // 全局日上限(使用自定义上限)
|
|
|
LambdaQueryWrapper<EnergyLog> globalWrapper = new LambdaQueryWrapper<EnergyLog>()
|
|
LambdaQueryWrapper<EnergyLog> globalWrapper = new LambdaQueryWrapper<EnergyLog>()
|
|
|
.eq(EnergyLog::getChildId, childId)
|
|
.eq(EnergyLog::getChildId, childId)
|
|
|
.gt(EnergyLog::getAmount, 0)
|
|
.gt(EnergyLog::getAmount, 0)
|
|
@@ -1461,7 +1604,8 @@ public class EnergyService {
|
|
|
Integer globalToday = energyLogMapper.selectList(globalWrapper).stream()
|
|
Integer globalToday = energyLogMapper.selectList(globalWrapper).stream()
|
|
|
.mapToInt(e -> e.getAmount() != null ? e.getAmount() : 0)
|
|
.mapToInt(e -> e.getAmount() != null ? e.getAmount() : 0)
|
|
|
.sum();
|
|
.sum();
|
|
|
- if (globalToday + amount > GLOBAL_DAILY_LIMIT) return true;
|
|
|
|
|
|
|
+ int globalLimit = customGlobalLimit != null ? customGlobalLimit : GLOBAL_DAILY_LIMIT;
|
|
|
|
|
+ if (globalToday + amount > globalLimit) return true;
|
|
|
|
|
|
|
|
// 各维度日上限
|
|
// 各维度日上限
|
|
|
for (Long dimId : dimIds) {
|
|
for (Long dimId : dimIds) {
|