| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357 |
- package com.etotem.cfc.service;
- import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
- import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
- import com.etotem.cfc.dto.CfBalanceView;
- import com.etotem.cfc.dto.EnergySandboxDTO;
- import com.etotem.cfc.dto.MemberEnergyDTO;
- import com.etotem.cfc.dto.DimensionNode;
- import com.etotem.cfc.entity.*;
- 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 org.springframework.context.annotation.Lazy;
- import org.springframework.stereotype.Service;
- import org.springframework.transaction.annotation.Transactional;
- import javax.annotation.Resource;
- import java.util.*;
- import java.util.stream.Collectors;
- import com.etotem.cfc.util.SortUtil;
- import com.etotem.cfc.service.NotificationDispatchService;
- /**
- * 家庭能量沙盘 — 核心计算服务
- * 计算每个家庭成员(家庭主账号+家庭成员)的身/心/智/行/富五维能量值 (0-100%),
- * 并聚合出家庭整体能量视图。
- */
- @Slf4j
- @Service
- public class EnergyService {
- @Resource
- private UserMapper userMapper;
- @Resource
- private FamilyMemberMapper familyMemberMapper;
- @Resource
- private NotificationDispatchService notificationDispatchService;
- @Resource
- private TaskMapper taskMapper;
- @Resource
- private PointsLogMapper pointsLogMapper;
- @Resource
- private GameRecordMapper gameRecordMapper;
- @Resource
- private DanAssessmentResultMapper danAssessmentResultMapper;
- @Resource
- private ArticleReadingRecordMapper articleReadingRecordMapper;
- @Resource
- private StreakMilestoneMapper streakMilestoneMapper;
- @Resource
- private HealthReportService healthReportService;
- @Resource
- private HealthReportMapper healthReportMapper;
- @Resource
- private FamilyMemberAttributesMapper familyMemberAttributesMapper;
- @Resource
- private FiveDimensionScoreService fiveDimensionScoreService;
- @Resource
- private HealthDimensionScoreService healthDimensionScoreService;
- /**
- * CF 值统一账本(F-2)。家庭主账号富维度「财务积累」改读个人账本 cf_balance.total_earned。
- *
- * <p>{@code @Lazy} 延迟注入:CfBalanceServiceImpl → MembershipService → … → CfBalanceService
- * 存在启动期循环依赖风险(见 commit c4813f72d / 02ca6aa0c),此处用代理延迟解析,
- * 运行时调用才真正取 bean,避免应用启动失败。</p>
- */
- @Resource
- @Lazy
- private CfBalanceService cfBalanceService;
- @Resource
- private ReferralTreeMapper referralTreeMapper;
- @Resource
- private GuideMapper guideMapper;
- @Resource
- private FamilyMetricAggConfigMapper familyMetricAggConfigMapper;
- /** 统一维度注册中心:五维根节点五行归属(wuxing_rule_ref)的权威来源 */
- @Resource
- private DimensionRegistryService dimensionRegistryService;
- /**
- * 计算整个家庭的能量沙盘数据
- *
- * @param familyId 家庭ID
- * @return 家庭能量沙盘DTO(家庭聚合 + 每个成员的个人数据)
- */
- public EnergySandboxDTO calculateFamilyEnergy(Long familyId) {
- if (familyId == null) {
- return buildEmptyDTO();
- }
- try {
- return doCalculateFamilyEnergy(familyId);
- } catch (Exception e) {
- log.error("计算家庭能量异常, familyId={}: {}", familyId, e.getMessage(), e);
- return buildEmptyDTO();
- }
- }
- private EnergySandboxDTO doCalculateFamilyEnergy(Long familyId) {
- EnergySandboxDTO dto = new EnergySandboxDTO();
- List<MemberEnergyDTO> members = new ArrayList<>();
- // 1. 收集所有家庭成员(查询全部用户,后续通过 hasParentRole 精确过滤 parent)
- // 注意:多角色用户(roles 包含 parent 但当前 role 切换为 child)不会被排除
- List<User> parents = userMapper.selectList(
- new LambdaQueryWrapper<User>()
- .eq(User::getFamilyId, familyId)
- );
- // 过滤掉单纯是 teacher/admin 角色且不是 parent 角色的用户
- // 注意:用户可能拥有多角色(如 parent + teacher),切换到 teacher 时 role 字段为 teacher,
- // 但只要 roles 列表中包含 parent,仍应作为家庭主账号参与能量计算
- List<User> familyParents = new ArrayList<>();
- for (User u : parents) {
- if (hasParentRole(u)) {
- familyParents.add(u);
- }
- }
- // family_members 可能包含“本人”的重复记录(user_id 指向家庭主账号),
- // 必须先按家庭主账号 userId 排除,并按家庭成员 userId 去重,否则同一家庭主账号会被重复计算。
- List<FamilyMember> children = getActualChildren(familyId);
- Date now = new Date();
- // 2. 计算每个家庭主账号的能量
- for (User parent : familyParents) {
- MemberEnergyDTO member = calcParentEnergy(parent);
- members.add(member);
- persistMemberScores(parent.getId(), "user", familyId, member, now);
- }
- // 3. 计算每个家庭成员的能量
- for (FamilyMember child : children) {
- MemberEnergyDTO member = calcChildEnergy(child);
- members.add(member);
- persistMemberScores(child.getId(), "member", familyId, member, now);
- }
- if (members.isEmpty()) {
- return buildEmptyDTO();
- }
- dto.setMembers(members);
- // 4. 聚合家庭五维(按配置 family_metric_agg_config 取 agg_type:AVG=平均 / SUM=合计 / COLLECT=汇集)
- // 默认 AVG 保证向后兼容;读不到配置时回落 AVG;COLLECT 对数值维度不适用,返回 AVG 并记录日志
- int bodySum = 0, mindSum = 0, wisdomSum = 0, actionSum = 0, wealthSum = 0;
- for (MemberEnergyDTO m : members) {
- bodySum += safeScore(m.getBodyScore());
- mindSum += safeScore(m.getMindScore());
- wisdomSum += safeScore(m.getWisdomScore());
- actionSum += safeScore(m.getActionScore());
- wealthSum += safeScore(m.getWealthScore());
- }
- int count = members.size();
- dto.setBodyScore(aggregateFamilyMetric("energy.body", bodySum, count));
- dto.setMindScore(aggregateFamilyMetric("energy.mind", mindSum, count));
- dto.setWisdomScore(aggregateFamilyMetric("energy.wisdom", wisdomSum, count));
- dto.setActionScore(aggregateFamilyMetric("energy.action", actionSum, count));
- dto.setWealthScore(aggregateFamilyMetric("energy.wealth", wealthSum, count));
- int overall = (dto.getBodyScore() + dto.getMindScore() + dto.getWisdomScore()
- + dto.getActionScore() + dto.getWealthScore()) / 5;
- dto.setOverallScore(overall);
- return dto;
- }
- /**
- * 按配置聚合家庭数值维度指标。
- *
- * <p>依据 {@code family_metric_agg_config} 中 metricKey 对应的 agg_type:
- * <ul>
- * <li>SUM → 合计(返回 sum,不除以 count)</li>
- * <li>COLLECT → 数值维度不适用,回落 AVG 并记录日志</li>
- * <li>AVG 或读不到配置 → 平均(默认,保持既有行为)</li>
- * </ul>
- */
- private int aggregateFamilyMetric(String metricKey, int sum, int count) {
- if (count <= 0) {
- return 0;
- }
- String aggType = resolveMetricAggType(metricKey);
- if ("SUM".equalsIgnoreCase(aggType)) {
- return sum;
- }
- if ("COLLECT".equalsIgnoreCase(aggType)) {
- log.info("配置项 {} 为 COLLECT 类型,对数值维度不适用,回落 AVG 计算", metricKey);
- }
- return sum / count;
- }
- /**
- * 读取某数据项的汇总方式(agg_type),读不到配置或配置无效时回落 AVG。
- */
- private String resolveMetricAggType(String metricKey) {
- if (metricKey == null) {
- return "AVG";
- }
- try {
- FamilyMetricAggConfig config = familyMetricAggConfigMapper.selectOne(
- new LambdaQueryWrapper<FamilyMetricAggConfig>()
- .eq(FamilyMetricAggConfig::getMetricKey, metricKey)
- .eq(FamilyMetricAggConfig::getStatus, "active")
- .last("LIMIT 1"));
- if (config != null && config.getAggType() != null && !config.getAggType().trim().isEmpty()) {
- return config.getAggType().trim().toUpperCase();
- }
- } catch (Exception e) {
- log.warn("读取家庭指标聚合配置失败 metricKey={}: {}", metricKey, e.getMessage());
- }
- return "AVG";
- }
- /**
- * 在家庭沙盘中查找指定成员的能量数据。
- * 匹配顺序:memberId+memberType → 仅 memberId → family_members.id 解析到的 userId(家庭主账号)。
- */
- public MemberEnergyDTO findMemberEnergy(Long familyId, Long memberId, String memberType) {
- if (familyId == null || memberId == null) {
- return null;
- }
- EnergySandboxDTO sandbox = calculateFamilyEnergy(familyId);
- if (sandbox == null || sandbox.getMembers() == null || sandbox.getMembers().isEmpty()) {
- return null;
- }
- List<MemberEnergyDTO> members = sandbox.getMembers();
- if (memberType != null && !memberType.isEmpty()) {
- for (MemberEnergyDTO m : members) {
- if (memberId.equals(m.getMemberId()) && memberType.equals(m.getMemberType())) {
- return m;
- }
- }
- }
- for (MemberEnergyDTO m : members) {
- if (memberId.equals(m.getMemberId())) {
- return m;
- }
- }
- FamilyMember fm = familyMemberMapper.selectById(memberId);
- if (fm != null && fm.getUserId() != null) {
- for (MemberEnergyDTO m : members) {
- if (fm.getUserId().equals(m.getMemberId()) && "parent".equals(m.getMemberType())) {
- return m;
- }
- }
- }
- return null;
- }
- /**
- * 计算当前用户个人能量沙盘(只含用户本人,不聚合家庭)
- */
- public EnergySandboxDTO calculatePersonalEnergy(Long userId) {
- if (userId == null) {
- return buildEmptyDTO();
- }
- try {
- User user = userMapper.selectById(userId);
- if (user == null) {
- return buildEmptyDTO();
- }
- EnergySandboxDTO dto = new EnergySandboxDTO();
- List<MemberEnergyDTO> members = new ArrayList<>();
- MemberEnergyDTO me = calcParentEnergy(user);
- members.add(me);
- dto.setMembers(members);
- dto.setBodyScore(safeScore(me.getBodyScore()));
- dto.setMindScore(safeScore(me.getMindScore()));
- dto.setWisdomScore(safeScore(me.getWisdomScore()));
- dto.setActionScore(safeScore(me.getActionScore()));
- dto.setWealthScore(safeScore(me.getWealthScore()));
- int overall = (safeScore(me.getBodyScore()) + safeScore(me.getMindScore())
- + safeScore(me.getWisdomScore()) + safeScore(me.getActionScore())
- + safeScore(me.getWealthScore())) / 5;
- dto.setOverallScore(overall);
- return dto;
- } catch (Exception e) {
- log.error("计算个人能量异常, userId={}: {}", userId, e.getMessage(), e);
- return buildEmptyDTO();
- }
- }
- private void persistMemberScores(Long memberId, String memberType, Long familyId,
- MemberEnergyDTO member, Date assessedAt) {
- recordDimension(memberId, memberType, familyId, "body", member.getBodyScore(), assessedAt);
- recordDimension(memberId, memberType, familyId, "mind", member.getMindScore(), assessedAt);
- recordDimension(memberId, memberType, familyId, "wisdom", member.getWisdomScore(), assessedAt);
- recordDimension(memberId, memberType, familyId, "action", member.getActionScore(), assessedAt);
- recordDimension(memberId, memberType, familyId, "wealth", member.getWealthScore(), assessedAt);
- }
- private void recordDimension(Long memberId, String memberType, Long familyId,
- String dimensionCode, Integer score, Date assessedAt) {
- if (score == null) return;
- try {
- fiveDimensionScoreService.recordSnapshot(
- memberId, memberType, familyId,
- dimensionCode, score, assessedAt,
- "periodic", null, null);
- } catch (Exception e) {
- log.warn("持久化五维分数失败 memberId={} dimension={}: {}", memberId, dimensionCode, e.getMessage());
- }
- }
- /**
- * 获取家庭中真正的家庭成员记录。
- * 排除 user_id 指向本人的重复记录,并对同一家庭成员账号只保留一条。
- */
- List<FamilyMember> getActualChildren(Long familyId) {
- if (familyId == null) return Collections.emptyList();
- List<FamilyMember> rows = familyMemberMapper.selectList(
- new LambdaQueryWrapper<FamilyMember>()
- .eq(FamilyMember::getFamilyId, familyId)
- );
- List<User> users = userMapper.selectList(
- new LambdaQueryWrapper<User>()
- .eq(User::getFamilyId, familyId)
- );
- Set<Long> parentUserIds = new HashSet<>();
- if (users != null) {
- for (User u : users) {
- if (hasParentRole(u)) parentUserIds.add(u.getId());
- }
- }
- return filterActualChildren(rows, parentUserIds);
- }
- static List<FamilyMember> filterActualChildren(List<FamilyMember> rows, Set<Long> parentUserIds) {
- List<FamilyMember> result = new ArrayList<>();
- if (rows == null) return result;
- Set<Long> seenChildUserIds = new HashSet<>();
- for (FamilyMember row : rows) {
- if (row == null) continue;
- Long userId = row.getUserId();
- if (userId != null) {
- if (parentUserIds != null && parentUserIds.contains(userId)) continue;
- if (!seenChildUserIds.add(userId)) continue;
- }
- result.add(row);
- }
- return result;
- }
- /**
- * 收集家庭内所有「数据归属主体」列表(供珍珠图/章鱼图家庭汇集接口复用)。
- * <p>
- * 与 {@link #doCalculateFamilyEnergy} 的成员收集逻辑一致:
- * 家庭主账号(userMapper 取 role=parent 的用户)+ 家庭成员(getActualChildren 去重)。
- * </p>
- * <p>每项字段:</p>
- * <ul>
- * <li>{@code memberId}:前端归属键(主账号=users.id;成员=family_members.id)</li>
- * <li>{@code memberName}:昵称</li>
- * <li>{@code memberType}:user=家庭主账号 / member=家庭成员(契约约定)</li>
- * <li>{@code colorKey}:稳定色值键(0..n-1 索引,同一成员前后一致)</li>
- * <li>{@code dataUserId}:拉取个人数据所用 userId(主账号=userId;成员=family_members.userId,可为 null)</li>
- * </ul>
- */
- public List<Map<String, Object>> collectFamilyMetricMembers(Long familyId) {
- List<Map<String, Object>> result = new ArrayList<>();
- if (familyId == null) {
- return result;
- }
- List<User> parents = userMapper.selectList(
- new LambdaQueryWrapper<User>()
- .eq(User::getFamilyId, familyId)
- .orderByAsc(User::getId));
- List<FamilyMember> children = getActualChildren(familyId);
- int colorIdx = 0;
- for (User parent : parents) {
- if (parent == null || !hasParentRole(parent)) {
- continue;
- }
- Map<String, Object> m = new LinkedHashMap<>();
- m.put("memberId", parent.getId());
- m.put("memberName", parent.getNickname() != null ? parent.getNickname() : "成员");
- m.put("memberType", "user");
- m.put("colorKey", colorIdx++);
- m.put("dataUserId", parent.getId());
- result.add(m);
- }
- for (FamilyMember child : children) {
- if (child == null) {
- continue;
- }
- Map<String, Object> m = new LinkedHashMap<>();
- m.put("memberId", child.getId());
- m.put("memberName", child.getNickname() != null ? child.getNickname() : "成员");
- m.put("memberType", "member");
- m.put("colorKey", colorIdx++);
- m.put("dataUserId", child.getUserId());
- result.add(m);
- }
- return result;
- }
- // ==================== 家庭主账号能量计算 ====================
- private MemberEnergyDTO calcParentEnergy(User parent) {
- MemberEnergyDTO dto = new MemberEnergyDTO();
- dto.setMemberId(parent.getId());
- dto.setMemberType("parent");
- dto.setName(parent.getNickname());
- dto.setAvatar(parent.getAvatar());
- dto.setFamilyRole(parent.getFamilyRole()); // 爸爸/妈妈/爷爷/奶奶
- // 家庭主账号五维计算(数据相对少,主要是任务完成率 + 积分)
- dto.setBodyScore(calcParentBody(parent));
- dto.setMindScore(calcParentMind(parent));
- dto.setWisdomScore(calcParentWisdom(parent));
- dto.setActionScore(calcParentAction(parent));
- dto.setWealthScore(calcParentWealth(parent, dto));
- // 心/行/智/富 子维度(家庭主账号暂无DAN数据,心13维回0;行6维/富传承留待自检问卷)
- setMindSubDimensionsForParent(dto);
- setActionSubDimensionsForParent(dto);
- setWisdomSubDimensionsForParent(dto);
- setWealthSubDimensionsForParent(dto);
- // v2.0 三轮算法:克环 → 生环(链式调用,避免迭代收敛)
- applyAllRestraints(dto);
- applyAllBoosts(dto);
- int overall = (safeScore(dto.getBodyScore()) + safeScore(dto.getMindScore())
- + safeScore(dto.getWisdomScore()) + safeScore(dto.getActionScore())
- + safeScore(dto.getWealthScore())) / 5;
- dto.setOverallScore(overall);
- return dto;
- }
- /**
- * 家庭主账号 身 — 运动健康类任务完成率
- * 注:家庭主账号如绑定了家庭成员(child ID),可通过该家庭成员的健康报告间接获取身体数据
- * Optimized: batch-fetch all children's health scores in a single query
- */
- private int calcParentBody(User parent) {
- // 1. 本人有 active 健康报告时,优先使用自己的数据,
- // 避免多个家庭主账号共用同一条家庭成员报告(此前所有家庭主账号 body 都会变成 75)。
- if (parent.getId() != null) {
- HealthReport ownReport = queryLatestActiveReport(Collections.singletonList(parent.getId()));
- if (ownReport != null && ownReport.getOverallScore() != null) {
- return ownReport.getOverallScore();
- }
- }
- if (parent.getFamilyId() != null) {
- List<FamilyMember> children = getActualChildren(parent.getFamilyId());
- // Batch query: fetch health reports for ALL actual children at once
- List<Long> childUserIds = children.stream()
- .map(FamilyMember::getUserId)
- .filter(Objects::nonNull)
- .distinct()
- .collect(Collectors.toList());
- if (!childUserIds.isEmpty()) {
- HealthReport report = queryLatestActiveReport(childUserIds);
- if (report != null && report.getOverallScore() != null) {
- return report.getOverallScore();
- }
- }
- // 7维健康维度评分加权:将所有家庭成员的7维评分取均值作为家庭主账号body能量
- List<HealthDimensionScore> allChildScores = new ArrayList<>();
- for (FamilyMember child : children) {
- try {
- List<HealthDimensionScore> scores = healthDimensionScoreService.getLatestScores(child.getId());
- if (scores != null) {
- allChildScores.addAll(scores);
- }
- } catch (Exception ignored) {}
- }
- if (!allChildScores.isEmpty()) {
- int avgScore = calcSevenDimBodyScoreFromScores(allChildScores);
- if (avgScore > 0) return avgScore;
- }
- }
- return calcTaskCompletionRate(parent.getId(), "parent",
- Arrays.asList("运动", "体育", "户外", "健康"), 365);
- }
- private HealthReport queryLatestActiveReport(List<Long> userIds) {
- if (userIds == null || userIds.isEmpty()) return null;
- LambdaQueryWrapper<HealthReport> wrapper = new LambdaQueryWrapper<HealthReport>()
- .in(HealthReport::getUserId, userIds)
- .eq(HealthReport::getStatus, "active")
- .isNotNull(HealthReport::getOverallScore)
- .orderByDesc(HealthReport::getReportDate)
- .last("LIMIT 1");
- try {
- SortUtil.applySort(wrapper);
- return healthReportMapper.selectOne(wrapper);
- } catch (Exception e) {
- log.warn("健康报告查询异常, userIds={}: {}", userIds, e.getMessage());
- return null;
- }
- }
- /** 家庭主账号 心 — 学习/阅读/亲子类任务完成率 */
- private int calcParentMind(User parent) {
- return calcTaskCompletionRate(parent.getId(), "parent",
- Arrays.asList("学习", "阅读", "亲子", "情感", "成长"), 365);
- }
- /** 家庭主账号 智 — 学习提升类任务完成率(家庭主账号没有游戏/测评数据) */
- private int calcParentWisdom(User parent) {
- int taskScore = calcTaskCompletionRate(parent.getId(), "parent",
- Arrays.asList("学习", "培训", "技能", "阅读", "知识"), 365);
- return taskScore;
- }
- /** 家庭主账号 行 — 习惯/自律类任务完成率 */
- private int calcParentAction(User parent) {
- return calcTaskCompletionRate(parent.getId(), "parent",
- Arrays.asList("习惯", "好习惯", "自律", "家务", "自理", "作息"), 365);
- }
- /** 家庭主账号 富 — 利他创富四子维度加权:个人能力25% + 利他贡献30% + 财务积累(CF值)35% + 财务素养10% */
- private int calcParentWealth(User parent, MemberEnergyDTO dto) {
- int skill = calcParentWealthSkill(parent);
- int altruism = calcParentWealthAltruism(parent);
- int income = calcParentWealthIncome(parent);
- int fx = calcParentWealthFx(parent);
- dto.setWealthSkill(skill);
- dto.setWealthAltruism(altruism);
- dto.setWealthIncome(income);
- dto.setWealthFx(fx);
- // 旧字段兼容(新前端改为读 wealthSkill/wealthAltruism/wealthIncome/wealthFx)
- dto.setWealthAchievement(skill);
- dto.setWealthNetwork(altruism);
- return (int) Math.round(skill * 0.25 + altruism * 0.3 + income * 0.35 + fx * 0.1);
- }
- /** 家庭主账号富-个人能力:服务商/规划师资质(active) 满分100,否则按成长类任务完成率 */
- private int calcParentWealthSkill(User parent) {
- Long guideCount = guideMapper.selectCount(
- new LambdaQueryWrapper<Guide>()
- .eq(Guide::getUserId, parent.getId())
- .eq(Guide::getStatus, "active"));
- if (guideCount != null && guideCount > 0) {
- return 100;
- }
- return calcTaskCompletionRate(parent.getId(), "parent",
- Arrays.asList("学习", "培训", "技能", "阅读", "知识"), 365);
- }
- /** 家庭主账号富-利他贡献:ReferralTree 直接邀请人数折算(1人=20分,每多1人+15分,最高100) */
- private int calcParentWealthAltruism(User parent) {
- Long count = referralTreeMapper.selectCount(
- new LambdaQueryWrapper<ReferralTree>().eq(ReferralTree::getParentId, parent.getId()));
- if (count == null || count == 0) return 0;
- return clamp(20 + (int) ((count - 1) * 15), 0, 100);
- }
- /**
- * 家庭主账号富-财务积累:个人 CF 值累计获得(cf_balance.total_earned)折算,5000CF=100分。
- *
- * <p>F-7 口径修复:CF 值已统一为个人维度账本(cf_balance,成员维度 family_members.id),
- * 此处不再读家庭池 family_platform_balance,避免与 C 端展示口径不一致。</p>
- */
- private int calcParentWealthIncome(User parent) {
- if (parent == null || parent.getId() == null) return 0;
- Long memberId = resolveMemberId(parent.getId());
- if (memberId == null) return 0;
- CfBalanceView balance = cfBalanceService.getBalance(memberId);
- if (balance == null || balance.getTotalEarned() == null) return 0;
- return clamp(balance.getTotalEarned() / 50, 0, 100);
- }
- /**
- * 登录用户 → 家庭成员(family_members.id)。
- *
- * <p>口径与 {@code CfEarnDispatchService}(F-3a)/ {@code CfBalanceController}(F-7a)一致:
- * user_id 匹配 + 按 id 升序取第一条(LIMIT 1),未命中返回 null。</p>
- */
- private Long resolveMemberId(Long userId) {
- if (userId == null || userId <= 0) {
- return null;
- }
- FamilyMember member = familyMemberMapper.selectOne(new LambdaQueryWrapper<FamilyMember>()
- .eq(FamilyMember::getUserId, userId)
- .orderByAsc(FamilyMember::getId)
- .last("LIMIT 1"));
- return member == null ? null : member.getId();
- }
- /** 家庭主账号富-财务素养:记账/理财/储蓄类任务完成率 */
- private int calcParentWealthFx(User parent) {
- return calcTaskCompletionRate(parent.getId(), "parent",
- Arrays.asList("记账", "理财", "财务", "储蓄", "投资"), 365);
- }
- // ==================== 家庭成员能量计算 ====================
- private MemberEnergyDTO calcChildEnergy(FamilyMember child) {
- MemberEnergyDTO dto = new MemberEnergyDTO();
- dto.setMemberId(child.getId());
- dto.setMemberType("child"); // 非登录用户(不可登录成员)
- dto.setName(child.getNickname());
- dto.setAvatar(null); // 家庭成员没有独立头像,用默认
- int bodyScore = calcChildBody(child);
- dto.setBodyScore(bodyScore);
- dto.setMindScore(calcChildMind(child));
- dto.setWisdomScore(calcChildWisdom(child));
- dto.setActionScore(calcChildAction(child));
- dto.setWealthScore(calcChildWealth(child, dto));
- // 心/行/智/富 子维度(家庭成员:心13维从DAN报告取,行6维/富传承留待自检问卷)
- setMindSubDimensionsForChild(dto, child);
- setActionSubDimensionsForChild(dto);
- setWisdomSubDimensionsForChild(dto, child);
- setWealthSubDimensionsForChild(dto);
- // v2.0 三轮算法:克环 → 生环(链式调用,避免迭代收敛)
- applyAllRestraints(dto);
- applyAllBoosts(dto);
- int overall = (safeScore(dto.getBodyScore()) + safeScore(dto.getMindScore())
- + safeScore(dto.getWisdomScore()) + safeScore(dto.getActionScore())
- + safeScore(dto.getWealthScore())) / 5;
- dto.setOverallScore(overall);
- return dto;
- }
- /**
- * 家庭成员 身 — 优先使用健康检测报告得分
- * 数据源优先级:健康报告overallScore > 子项加权 > 运动/户外类任务完成率(降级)
- */
- private int calcChildBody(FamilyMember child) {
- // 1. 优先使用健康报告中的身体评分
- Integer healthScore = healthReportService.calculateBodyScore(child.getUserId());
- if (healthScore != null) {
- return healthScore;
- }
- // 2. 7维健康维度评分加权(T4: 7-Dim→Body Energy)
- int sevenDimScore = calcSevenDimBodyScore(child.getId());
- if (sevenDimScore > 0) {
- return sevenDimScore;
- }
- // 3. 降级:运动/户外类任务完成率
- return calcTaskCompletionRate(child.getId(), "child",
- Arrays.asList("运动", "体育", "户外", "健康"), 365);
- }
- /**
- * 7维健康维度评分加权 → body 能量 (T4/T12)
- * 从 health_dimension_scores 表读取成员最新的所有维度评分,
- * 按设计权重加权计算 0-100 分的综合 body 能量。
- * 各维度权重参考 specs/2026-06-26-health-dimensions-design-spec.md:
- * 肠胃(20%) 营养(18%) 运动(15%) 睡眠(15%) 生长(12%) 免疫(10%) 视力(10%)
- */
- private int calcSevenDimBodyScore(Long memberId) {
- try {
- List<HealthDimensionScore> scores = healthDimensionScoreService.getLatestScores(memberId);
- if (scores == null || scores.isEmpty()) {
- return 0;
- }
- double weightedSum = 0;
- double totalWeight = 0;
- java.util.Map<String, Double> weights = new java.util.HashMap<>();
- weights.put("gut", 0.20);
- weights.put("nutrition", 0.18);
- weights.put("exercise", 0.15);
- weights.put("sleep", 0.15);
- weights.put("growth", 0.12);
- weights.put("immunity", 0.10);
- weights.put("vision", 0.10);
- for (HealthDimensionScore s : scores) {
- Double w = weights.get(s.getDimension());
- if (w != null && s.getScore() != null) {
- weightedSum += s.getScore() * w;
- totalWeight += w;
- }
- }
- if (totalWeight == 0) {
- return 0;
- }
- return (int) Math.round(weightedSum / totalWeight);
- } catch (Exception e) {
- log.warn("7维健康维度评分加权失败, memberId={}: {}", memberId, e.getMessage());
- return 0;
- }
- }
- private int calcSevenDimBodyScoreFromScores(List<HealthDimensionScore> scores) {
- if (scores == null || scores.isEmpty()) return 0;
- java.util.Map<String, Double> weights = new java.util.HashMap<>();
- weights.put("gut", 0.20);
- weights.put("nutrition", 0.18);
- weights.put("exercise", 0.15);
- weights.put("sleep", 0.15);
- weights.put("growth", 0.12);
- weights.put("immunity", 0.10);
- weights.put("vision", 0.10);
- double weightedSum = 0;
- double totalWeight = 0;
- for (HealthDimensionScore s : scores) {
- Double w = weights.get(s.getDimension());
- if (w != null && s.getScore() != null) {
- weightedSum += s.getScore() * w;
- totalWeight += w;
- }
- }
- if (totalWeight == 0) return 0;
- return (int) Math.round(weightedSum / totalWeight);
- }
- /** 家庭成员 心 — DAN EMI 70% + 先天 30% + 行为修正 */
- private int calcChildMind(FamilyMember child) {
- Long memberId = child.getId();
- Long userId = child.getUserId();
- // 1. DAN EMI score
- DanAssessmentResult dan = findLatestDanResult(memberId);
- double danScore = -1;
- if (dan != null) {
- Integer ems = dan.getEmotionManagementScore();
- Integer es = dan.getEmpathyScore();
- Integer sas = dan.getSocialAdaptabilityScore();
- Integer sms = dan.getSelfMotivationScore();
- if (ems != null && es != null && sas != null && sms != null) {
- danScore = (ems + es + sas + sms) / 4.0;
- }
- }
- // 2. Innate score (先天)
- double innateScore = calcInnateScore(userId, "mind");
- // 3. Behavior modifier
- double modifier = calcBehaviorModifier(memberId, "mind");
- // 4. Model: has DAN
- if (danScore >= 0) {
- double base = danScore * 0.70;
- if (innateScore >= 0) {
- base += innateScore * 0.30;
- }
- return clamp((int) Math.round(base * (1 + modifier)), 0, 100);
- }
- // 5. Fallback: innate only
- if (innateScore >= 0) {
- return clamp((int) Math.round(innateScore * (1 + modifier)), 0, 100);
- }
- // 6. Fallback: task completion rate (old logic)
- return calcTaskCompletionRate(memberId, "child",
- Arrays.asList("学习", "阅读", "亲子"), 365);
- }
- /** 家庭成员 智 — DAN COG 70% + 先天 30% + 行为修正 */
- private int calcChildWisdom(FamilyMember child) {
- Long memberId = child.getId();
- Long userId = child.getUserId();
- // 1. DAN COG score - weighted average of 6 cognitive dimensions
- DanAssessmentResult dan = findLatestDanResult(memberId);
- double danScore = -1;
- if (dan != null) {
- Integer as = dan.getAttentionScore();
- Integer fs = dan.getFocusScore();
- Integer ms = dan.getMemoryScore();
- Integer ls = dan.getLogicScore();
- Integer ps = dan.getPerceptionScore();
- Integer ss = dan.getSpatialScore();
- if (as != null && fs != null && ms != null && ls != null && ps != null && ss != null) {
- danScore = as * 0.15 + fs * 0.15 + ms * 0.20 + ls * 0.20 + ps * 0.15 + ss * 0.15;
- }
- }
- // 2. Innate score
- double innateScore = calcInnateScore(userId, "wisdom");
- // 3. Behavior modifier
- double modifier = calcBehaviorModifier(memberId, "wisdom");
- // 4. Model: has DAN
- if (danScore >= 0) {
- double base = danScore * 0.70;
- if (innateScore >= 0) {
- base += innateScore * 0.30;
- }
- return clamp((int) Math.round(base * (1 + modifier)), 0, 100);
- }
- // 5. Fallback: innate only
- if (innateScore >= 0) {
- return clamp((int) Math.round(innateScore * (1 + modifier)), 0, 100);
- }
- // 6. Fallback: old multi-source weighted calculation
- return calcOldChildWisdom(child);
- }
- /** Find the latest completed DAN assessment for a child */
- private DanAssessmentResult findLatestDanResult(Long memberId) {
- LambdaQueryWrapper<DanAssessmentResult> qw = new LambdaQueryWrapper<DanAssessmentResult>()
- .eq(DanAssessmentResult::getMemberId, memberId)
- .eq(DanAssessmentResult::getStatus, "completed")
- .orderByDesc(DanAssessmentResult::getAssessmentDate)
- .last("LIMIT 1");
- SortUtil.applySort(qw);
- return danAssessmentResultMapper.selectOne(qw);
- }
- /** Calculate innate attribute base score for a dimension */
- private double calcInnateScore(Long userId, String dimensionCode) {
- FamilyMemberAttributes attrs = familyMemberAttributesMapper.selectOne(
- new LambdaQueryWrapper<FamilyMemberAttributes>()
- .eq(FamilyMemberAttributes::getMemberId, userId)
- .eq(FamilyMemberAttributes::getMemberType, "parent")
- );
- if (attrs == null) return -1;
- if ("mind".equals(dimensionCode) && attrs.getMindBaseScore() != null) {
- return attrs.getMindBaseScore().doubleValue();
- }
- if ("wisdom".equals(dimensionCode) && attrs.getWisdomBaseScore() != null) {
- return attrs.getWisdomBaseScore().doubleValue();
- }
- return -1;
- }
- /** Calculate behavior modifier based on recent activity */
- private double calcBehaviorModifier(Long memberId, String dimensionCode) {
- double modifier = 0;
- // Task completion rate (±5%)
- int taskRate = calcTaskCompletionRate(memberId, "child",
- Arrays.asList("学习", "阅读", "亲子", "好习惯", "运动"), 90);
- modifier += (taskRate - 50) / 100.0 * 0.05;
- // Game performance (±5%)
- LambdaQueryWrapper<GameRecord> gw = new LambdaQueryWrapper<GameRecord>()
- .eq(GameRecord::getMemberId, memberId);
- SortUtil.applySort(gw);
- List<GameRecord> games = gameRecordMapper.selectList(gw);
- if (!games.isEmpty()) {
- double avgScore = games.stream()
- .filter(g -> g.getScore() != null)
- .mapToInt(GameRecord::getScore)
- .average()
- .orElse(50);
- modifier += (avgScore - 50) / 100.0 * 0.05;
- }
- // Clamp to ±0.20
- return Math.max(-0.20, Math.min(0.20, modifier));
- }
- /** Old wisdom calculation as fallback (游戏 + 测评overallScore + 阅读时长) */
- private int calcOldChildWisdom(FamilyMember child) {
- Long memberId = child.getId();
- double totalWeight = 0;
- double weightedSum = 0;
- // 1. 游戏得分 (0-100, 平均分)
- LambdaQueryWrapper<GameRecord> gw = new LambdaQueryWrapper<GameRecord>()
- .eq(GameRecord::getMemberId, memberId);
- SortUtil.applySort(gw);
- List<GameRecord> games = gameRecordMapper.selectList(gw);
- if (!games.isEmpty()) {
- double avgScore = games.stream()
- .filter(g -> g.getScore() != null)
- .mapToInt(GameRecord::getScore)
- .average()
- .orElse(0);
- weightedSum += Math.min(avgScore, 100) * 0.34;
- totalWeight += 0.34;
- }
- // 2. 最近一次完成的测评得分
- DanAssessmentResult result = findLatestDanResult(memberId);
- if (result != null && result.getOverallScore() != null) {
- weightedSum += result.getOverallScore().doubleValue() * 0.33;
- totalWeight += 0.33;
- }
- // 3. 近30天阅读时长 (每3600秒=1小时=满分)
- Calendar cal = Calendar.getInstance();
- cal.add(Calendar.DAY_OF_MONTH, -30);
- Date thirtyDaysAgo = cal.getTime();
- Integer totalSeconds = articleReadingRecordMapper
- .selectTotalDurationByChildSince(memberId, thirtyDaysAgo);
- if (totalSeconds != null && totalSeconds > 0) {
- double readingScore = Math.min((double) totalSeconds / 3600 * 100, 100);
- weightedSum += readingScore * 0.33;
- totalWeight += 0.33;
- }
- if (totalWeight == 0) return 0;
- int score = (int) Math.round(weightedSum / totalWeight);
- return Math.min(Math.max(score, 0), 100);
- }
- private int clamp(int value, int min, int max) {
- return Math.max(min, Math.min(max, value));
- }
- /** 家庭成员 行 — 打卡天数 + 习惯类任务完成率 */
- private int calcChildAction(FamilyMember child) {
- Long memberId = child.getId();
- // 1. 连续打卡天数 (0-100)
- int streakDays = child.getStreakDays() != null ? child.getStreakDays() : 0;
- // 获取最高里程碑天数
- LambdaQueryWrapper<StreakMilestone> mw = new LambdaQueryWrapper<StreakMilestone>()
- .orderByDesc(StreakMilestone::getDays)
- .last("LIMIT 1");
- SortUtil.applySort(mw);
- List<StreakMilestone> milestones = streakMilestoneMapper.selectList(mw);
- int maxMilestone = 30;
- if (!milestones.isEmpty() && milestones.get(0).getDays() != null) {
- maxMilestone = milestones.get(0).getDays();
- }
- int dynamicRef = Math.max(streakDays, maxMilestone);
- int streakScore = Math.min(streakDays * 100 / Math.max(dynamicRef, 1), 100);
- // 2. 习惯类任务完成率
- int habitScore = calcTaskCompletionRate(memberId, "child",
- Arrays.asList("好习惯", "习惯", "自理", "家务"), 365);
- // 加权:打卡40% + 习惯任务60%
- return Math.min((int) Math.round(streakScore * 0.4 + habitScore * 0.6), 100);
- }
- /** 家庭成员 富 — 利他创富四子维度加权:学业能力25% + 利他贡献30% + 积分积累35% + 财务素养10% */
- private int calcChildWealth(FamilyMember child, MemberEnergyDTO dto) {
- int edu = calcChildWealthEducation(child);
- int altruism = calcChildWealthAltruism(child);
- int points = calcChildWealthPoints(child);
- int fx = calcChildWealthFx(child);
- dto.setWealthSkill(edu);
- dto.setWealthAltruism(altruism);
- dto.setWealthIncome(points);
- dto.setWealthFx(fx);
- // 旧字段兼容(新前端改为读 wealthSkill/wealthAltruism/wealthIncome/wealthFx)
- dto.setWealthEducation(edu);
- dto.setWealthSocial(altruism);
- dto.setWealthPoints(points);
- return (int) Math.round(edu * 0.25 + altruism * 0.3 + points * 0.35 + fx * 0.1);
- }
- /** 家庭成员富-学业能力:DAN测评最近3次综合分+进步趋势 */
- private int calcChildWealthEducation(FamilyMember child) {
- Long memberId = child.getId();
- LambdaQueryWrapper<DanAssessmentResult> qw = new LambdaQueryWrapper<DanAssessmentResult>()
- .eq(DanAssessmentResult::getMemberId, memberId)
- .eq(DanAssessmentResult::getStatus, "completed")
- .orderByDesc(DanAssessmentResult::getAssessmentDate)
- .last("LIMIT 3");
- SortUtil.applySort(qw);
- List<DanAssessmentResult> results = danAssessmentResultMapper.selectList(qw);
- if (results.isEmpty()) return 0;
- double avg = results.stream()
- .filter(r -> r.getOverallScore() != null)
- .mapToInt(DanAssessmentResult::getOverallScore)
- .average()
- .orElse(0);
- // 进步趋势:最近一次 vs 前面平均
- if (results.size() >= 2 && results.get(0).getOverallScore() != null && results.get(1).getOverallScore() != null) {
- int latest = results.get(0).getOverallScore();
- double prevAvg = results.subList(1, results.size()).stream()
- .filter(r -> r.getOverallScore() != null)
- .mapToInt(DanAssessmentResult::getOverallScore)
- .average()
- .orElse(latest);
- double trend = (latest - prevAvg) / Math.max(prevAvg, 1) * 10;
- avg += trend;
- }
- return clamp((int) Math.round(avg), 0, 100);
- }
- /** 家庭成员富-利他贡献:帮助他人/分享/公益/家务类任务完成率 */
- private int calcChildWealthAltruism(FamilyMember child) {
- return calcTaskCompletionRate(child.getId(), "child",
- Arrays.asList("帮助", "分享", "公益", "家务", "合作"), 365);
- }
- /** 家庭成员富-财务素养:财商/记账/理财/储蓄类任务完成率 */
- private int calcChildWealthFx(FamilyMember child) {
- return calcTaskCompletionRate(child.getId(), "child",
- Arrays.asList("财商", "记账", "理财", "储蓄", "投资"), 365);
- }
- /** 家庭成员富-规则博弈:积分获取效率 */
- private int calcChildWealthPoints(FamilyMember child) {
- Long memberId = child.getId();
- Calendar cal = Calendar.getInstance();
- cal.add(Calendar.YEAR, -1);
- Date oneYearAgo = cal.getTime();
- LambdaQueryWrapper<PointsLog> earnedWrapper = new LambdaQueryWrapper<PointsLog>()
- .eq(PointsLog::getMemberId, memberId)
- .gt(PointsLog::getAmount, 0)
- .gt(PointsLog::getCreatedAt, oneYearAgo);
- SortUtil.applySort(earnedWrapper);
- List<PointsLog> earnedLogs = pointsLogMapper.selectList(earnedWrapper);
- int earned = earnedLogs.stream()
- .mapToInt(pl -> pl.getAmount() != null ? pl.getAmount() : 0)
- .sum();
- LambdaQueryWrapper<Task> taskWrapper = new LambdaQueryWrapper<Task>()
- .eq(Task::getMemberId, memberId)
- .ne(Task::getIsTemplate, 1)
- .ne(Task::getStatus, "cancelled")
- .gt(Task::getCreatedAt, oneYearAgo);
- SortUtil.applySort(taskWrapper);
- List<Task> tasks = taskMapper.selectList(taskWrapper);
- int available = tasks.stream()
- .mapToInt(t -> t.getPoints() != null ? t.getPoints() : 0)
- .sum();
- int score = (int) Math.min((long) earned * 100 / Math.max(available, 1), 100);
- return Math.max(score, 0);
- }
- /**
- * 身克富杠杆:身体底盘制约财富能量(ke-full-design §克② 重写版)
- * - body=0 && wealth=0 → normal(无数据边界保护)
- * - 身<50 且 富>身+20 → overdraw(透支预警,不下调)
- * - 身<40 → penalty(wealth *= 0.5 + 0.5×身/100,文案"健康正在稀释财富能量")
- * - 否则 → normal(身≥70 且 富≥60:双优;身≥70 且 富<40:创富时机;其他:空)
- */
- private int applyBodyWealthRestraint(Integer bodyScore, Integer wealthScore, MemberEnergyDTO dto) {
- int body = safeScore(bodyScore);
- int wealth = safeScore(wealthScore);
- if (body == 0 && wealth == 0) {
- dto.setBodyWealthStatus("normal");
- dto.setBodyWealthMessage("");
- return wealth;
- }
- if (body < 50 && wealth > body + 20) {
- dto.setBodyWealthStatus("overdraw");
- dto.setBodyWealthMessage("财富能量超出身体承受范围,注意劳逸结合");
- return wealth;
- }
- if (body < 40) {
- double factor = 0.5 + 0.5 * body / 100.0;
- int adjusted = (int) Math.round(wealth * factor);
- dto.setBodyWealthStatus("penalty");
- dto.setBodyWealthMessage("健康正在稀释财富能量,建议优先关注身体健康");
- return Math.max(adjusted, 0);
- }
- dto.setBodyWealthStatus("normal");
- if (body >= 70 && wealth >= 60) {
- dto.setBodyWealthMessage("身体是财富的底盘,您处于双优状态");
- } else if (body >= 70 && wealth < 40) {
- dto.setBodyWealthMessage("您的健康基础很好,是启动创富的好时机");
- } else {
- dto.setBodyWealthMessage("");
- }
- return wealth;
- }
- // ==================== 五行相克(v2.0 克环其余4个 + 统一入口) ====================
- /**
- * 行克身(木克土)—— 行动社交对身体健康的制约
- * - 行>75 且 身<40 → overdraw(身 *= 0.5 + 0.5×身/100,透支)
- * - 行>60 且 身>60 → balanced(不下调,身心兼修)
- * - 身>70 且 行<30 → cautious(不下调,缺少行动提示)
- * - 否则 → normal
- */
- private int applyActionBodyRestraint(Integer actionScore, Integer bodyScore, MemberEnergyDTO dto) {
- int action = safeScore(actionScore);
- int body = safeScore(bodyScore);
- if (action > 75 && body < 40) {
- double penalty = 0.5 + 0.5 * (body / 100.0);
- int adjusted = (int) Math.round(body * penalty);
- dto.setActionBodyStatus("overdraw");
- dto.setActionBodyMessage("行动力很强,但身体在报警。适度暂停,是另一种前进");
- return Math.min(adjusted, body);
- }
- if (action > 60 && body > 60) {
- dto.setActionBodyStatus("balanced");
- dto.setActionBodyMessage("身心兼修,动态平衡");
- return body;
- }
- if (body > 70 && action < 30) {
- dto.setActionBodyStatus("cautious");
- dto.setActionBodyMessage("身体底子好,但缺少行动。出门走走,也是养生的一部分");
- return body;
- }
- dto.setActionBodyStatus("normal");
- dto.setActionBodyMessage("");
- return body;
- }
- /**
- * 富克心(水克火)—— 财富对心(情绪/包容/传承)三层面的侵蚀
- * 检测优先级:情绪<40→eruption / 包容<40→judgmental / 传承<20→materialized / 富>70心>65→nourished / normal
- */
- private int applyWealthHeartRestraint(Integer wealthScore, Integer heartScore, MemberEnergyDTO dto) {
- int wealth = safeScore(wealthScore);
- int heart = safeScore(heartScore);
- int emotionStable = dto.getHeartEmotionStable() != null ? dto.getHeartEmotionStable() : 50;
- int understanding = dto.getHeartUnderstanding() != null ? dto.getHeartUnderstanding() : 50;
- int legacy = dto.getHeartLegacy() != null ? dto.getHeartLegacy() : 50;
- if (wealth > 70 && emotionStable < 40) {
- double penalty = 0.4 + 0.6 * (emotionStable / 100.0);
- int adjusted = (int) Math.round(heart * penalty);
- dto.setWealthHeartStatus("eruption");
- dto.setWealthHeartMessage("财富给了底气,也偷走了对家人的耐心。先稳住情绪,再谈其他");
- return Math.min(adjusted, heart);
- }
- if (wealth > 60 && understanding < 40) {
- dto.setWealthHeartStatus("judgmental");
- dto.setWealthHeartMessage("站在高处久了,可能忘了普通人走路的难处");
- return heart;
- }
- if (wealth > 60 && legacy < 20) {
- dto.setWealthHeartStatus("materialized");
- dto.setWealthHeartMessage("留给下一代的只有存款,价值观也需要传下去");
- return heart;
- }
- if (wealth > 70 && heart > 65) {
- dto.setWealthHeartStatus("nourished");
- dto.setWealthHeartMessage("您的财富正在滋养家庭情感,这是最好的传承");
- return heart;
- }
- dto.setWealthHeartStatus("normal");
- dto.setWealthHeartMessage("");
- return heart;
- }
- /**
- * 心克智(火克金)—— 情感关爱对理性培养的制约
- * - 心>70 且 智<40 → overprotect(智 *= 0.5+0.5×智/100,过度保护)
- * - 智>70 且 心<40 → cold_wise(不下调,理性冷淡提示)
- * - 心>60 且 智>60 → balanced(不下调)
- * - 否则 → normal
- */
- private int applyMindWisdomRestraint(Integer mindScore, Integer wisdomScore, MemberEnergyDTO dto) {
- int mind = safeScore(mindScore);
- int wisdom = safeScore(wisdomScore);
- if (mind > 70 && wisdom < 40) {
- double penalty = 0.5 + 0.5 * (wisdom / 100.0);
- int adjusted = (int) Math.round(wisdom * penalty);
- dto.setMindWisdomStatus("overprotect");
- dto.setMindWisdomMessage("放手让家人试错,是培养智慧的第一步");
- return Math.min(adjusted, wisdom);
- }
- if (wisdom > 70 && mind < 40) {
- dto.setMindWisdomStatus("cold_wise");
- dto.setMindWisdomMessage("智慧需要温度,不然长大了只会做事不会做人");
- return wisdom;
- }
- if (mind > 60 && wisdom > 60) {
- dto.setMindWisdomStatus("balanced");
- dto.setMindWisdomMessage("爱与理性并行,是最好的教育");
- return wisdom;
- }
- dto.setMindWisdomStatus("normal");
- dto.setMindWisdomMessage("");
- return wisdom;
- }
- /**
- * 智克行(金克木)—— 认知分析对人际关系的制约
- * - 智<30 且 行>50 → blind(行 *= 0.6+0.4×智/100,盲目社交下调)
- * - 智>80 且 行<40 → paralysis(不下调,认知孤独预警)
- * - 智>60 且 行>60 → balanced(不下调)
- * - 否则 → normal
- */
- private int applyWisdomActionRestraint(Integer wisdomScore, Integer actionScore, MemberEnergyDTO dto) {
- int wisdom = safeScore(wisdomScore);
- int action = safeScore(actionScore);
- if (wisdom < 30 && action > 50) {
- double penalty = 0.6 + 0.4 * (wisdom / 100.0);
- int adjusted = (int) Math.round(action * penalty);
- dto.setWisdomActionStatus("blind");
- dto.setWisdomActionMessage("行动力很强,但有方向会让努力更有效。建议先做一次测评");
- return Math.min(adjusted, action);
- }
- if (wisdom > 80 && action < 40) {
- dto.setWisdomActionStatus("paralysis");
- dto.setWisdomActionMessage("聪明不等于会相处。朋友不是用逻辑交来的");
- return action;
- }
- if (wisdom >= 60 && action >= 60) {
- dto.setWisdomActionStatus("balanced");
- dto.setWisdomActionMessage("知行合一状态,继续保持");
- return action;
- }
- dto.setWisdomActionStatus("normal");
- if (action >= 60 && wisdom < 50) {
- dto.setWisdomActionMessage("行动力很好,建议增加测评了解方向");
- } else {
- dto.setWisdomActionMessage("");
- }
- return action;
- }
- /**
- * 五行相克统一入口 — 按克环顺序:行克身→身克富→富克心→心克智→智克行。
- * 关键:每个克方法使用前一个克方法调整后的分值(链式调用,避免迭代收敛问题)。
- */
- private void applyAllRestraints(MemberEnergyDTO dto) {
- int bodyAdj = applyActionBodyRestraint(dto.getActionScore(), dto.getBodyScore(), dto);
- dto.setBodyScore(bodyAdj);
- int wealthAdj = applyBodyWealthRestraint(dto.getBodyScore(), dto.getWealthScore(), dto);
- dto.setWealthScore(wealthAdj);
- int mindAdj = applyWealthHeartRestraint(dto.getWealthScore(), dto.getMindScore(), dto);
- dto.setMindScore(mindAdj);
- int wisdomAdj = applyMindWisdomRestraint(dto.getMindScore(), dto.getWisdomScore(), dto);
- dto.setWisdomScore(wisdomAdj);
- int actionAdj = applyWisdomActionRestraint(dto.getWisdomScore(), dto.getActionScore(), dto);
- dto.setActionScore(actionAdj);
- }
- // ==================== 五行相生(v2.0 生环5个 + 统一入口) ====================
- /**
- * 生增益统一公式:boost = (upstream-50)/100×10,仅当 upstream>60 且 downstream<80 触发,
- * 上限 80-downstream(留成长空间)。
- */
- private int applyBoost(Integer upstreamScore, Integer downstreamScore) {
- int up = safeScore(upstreamScore);
- int down = safeScore(downstreamScore);
- if (up > 60 && down < 80) {
- double boost = (up - 50) / 100.0 * 10;
- int room = 80 - down;
- int gain = (int) Math.round(boost);
- if (gain > room) gain = room;
- if (gain < 0) gain = 0;
- return down + gain;
- }
- return down;
- }
- /** 心生身(火生土) */
- private int applyMindBodyBoost(Integer mindScore, Integer bodyScore, MemberEnergyDTO dto) {
- return applyBoost(mindScore, bodyScore);
- }
- /** 身生智(土生金) */
- private int applyBodyWisdomBoost(Integer bodyScore, Integer wisdomScore, MemberEnergyDTO dto) {
- return applyBoost(bodyScore, wisdomScore);
- }
- /** 智生富(金生水) */
- private int applyWisdomWealthBoost(Integer wisdomScore, Integer wealthScore, MemberEnergyDTO dto) {
- return applyBoost(wisdomScore, wealthScore);
- }
- /** 富生行(水生木) */
- private int applyWealthActionBoost(Integer wealthScore, Integer actionScore, MemberEnergyDTO dto) {
- return applyBoost(wealthScore, actionScore);
- }
- /** 行生心(木生火) */
- private int applyActionMindBoost(Integer actionScore, Integer mindScore, MemberEnergyDTO dto) {
- return applyBoost(actionScore, mindScore);
- }
- /**
- * 五行相生统一入口 — 按生环顺序:心生身→身生智→智生富→富生行→行生心。
- * 链式调用,每个生方法使用前一个的输出作为下游输入。
- */
- private void applyAllBoosts(MemberEnergyDTO dto) {
- dto.setBodyScore(applyMindBodyBoost(dto.getMindScore(), dto.getBodyScore(), dto));
- dto.setWisdomScore(applyBodyWisdomBoost(dto.getBodyScore(), dto.getWisdomScore(), dto));
- dto.setWealthScore(applyWisdomWealthBoost(dto.getWisdomScore(), dto.getWealthScore(), dto));
- dto.setActionScore(applyWealthActionBoost(dto.getWealthScore(), dto.getActionScore(), dto));
- dto.setMindScore(applyActionMindBoost(dto.getActionScore(), dto.getMindScore(), dto));
- }
- // ==================== 心的子维度(v2.0 心三层拆分) ====================
- /** 心·情绪稳定 — 家庭成员:DAN EMI 三子分(emotionManagement+resilience+stressCoping)均值。
- * 文档原 emotionCheckinService 在本项目不存在,用此现有 DAN 字段做代理实现。 */
- private int calcHeartEmotionStableForChild(Long memberId, DanAssessmentResult dan) {
- if (dan == null) return 50;
- Integer ems = dan.getEmotionManagementScore();
- Integer resilience = dan.getResilienceScore();
- Integer stress = dan.getStressCopingScore();
- if (ems == null || resilience == null || stress == null) return 50;
- return clamp((ems + resilience + stress) / 3, 0, 100);
- }
- /** 心·情绪稳定 — 家庭主账号:暂无数据,回0(ke-full-design 维度表显式标注) */
- private int calcHeartEmotionStableForParent() {
- return 0;
- }
- /** 心·理解包容 — 家庭成员:DAN (empathy+socialAdaptability)/2。
- * 文档原家庭天盘 helper 不存在,按 doc §克③ line 295-296 用 100% DAN fallback。 */
- private int calcHeartUnderstandingForChild(Long memberId, DanAssessmentResult dan) {
- if (dan == null) return 50;
- Integer empathy = dan.getEmpathyScore();
- Integer social = dan.getSocialAdaptabilityScore();
- if (empathy == null || social == null) return 50;
- return clamp((empathy + social) / 2, 0, 100);
- }
- /** 心·理解包容 — 家庭主账号:基于连续打卡天数折算(每30天≈50分,最高100) */
- private int calcHeartUnderstandingForParent() {
- return 50;
- }
- /** 心·传承传递 — 基于家庭主账号连续打卡天数(每60天≈50分,最高100) */
- private int calcHeartLegacy() {
- return 50;
- }
- // ==================== 新五维子维度设置方法 ====================
- /** 心13维 — 家庭主账号版:暂无DAN数据,全部回0,旧字段兼容回填 */
- private void setMindSubDimensionsForParent(MemberEnergyDTO dto) {
- dto.setBigFiveOpenness(0);
- dto.setBigFiveConscientiousness(0);
- dto.setBigFiveExtraversion(0);
- dto.setBigFiveAgreeableness(0);
- dto.setBigFiveNeuroticism(0);
- dto.setEmotionEmotion(0);
- dto.setEmotionResilience(0);
- dto.setEmotionStressCoping(0);
- dto.setEmotionSelfAwareness(0);
- dto.setDriveAutonomy(0);
- dto.setDriveCompetence(0);
- dto.setDriveRelatedness(0);
- dto.setSelfConceptOverall(0);
- // 旧字段兼容
- dto.setHeartEmotionStable(0);
- dto.setHeartUnderstanding(0);
- dto.setHeartLegacy(0);
- }
- /** 心13维 — 家庭成员版:从 DAN 报告取大五+EMI,从 structuredAnalysis 取自驱力+自我概念 */
- private void setMindSubDimensionsForChild(MemberEnergyDTO dto, FamilyMember child) {
- DanAssessmentResult dan = findLatestDanResult(child.getId());
- if (dan != null) {
- // 大五人格(5维)
- dto.setBigFiveOpenness(safeScore(dan.getOpennessScore()));
- dto.setBigFiveConscientiousness(safeScore(dan.getConscientiousnessScore()));
- dto.setBigFiveExtraversion(safeScore(dan.getExtraversionScore()));
- dto.setBigFiveAgreeableness(safeScore(dan.getAgreeablenessScore()));
- dto.setBigFiveNeuroticism(safeScore(dan.getNeuroticismScore()));
- // 情绪稳定(4维)
- dto.setEmotionEmotion(safeScore(dan.getEmotionScore()));
- dto.setEmotionResilience(safeScore(dan.getResilienceScore()));
- dto.setEmotionStressCoping(safeScore(dan.getStressCopingScore()));
- dto.setEmotionSelfAwareness(safeScore(dan.getSelfAwarenessScore()));
- // 情绪稳定性 = 四个 EMI 维度均值
- int emiSum = safeScore(dan.getEmotionScore()) + safeScore(dan.getResilienceScore())
- + safeScore(dan.getStressCopingScore()) + safeScore(dan.getSelfAwarenessScore());
- dto.setEmotionStability(emiSum / 4);
- // 自驱力(3维)+ 自我概念(1维)— 从 structuredAnalysis JSON 嵌套 extra 解析
- Map<String, Object> analysis = parseStructuredAnalysis(dan.getStructuredAnalysis());
- if (analysis != null) {
- parseSelfDrivingFromAnalysis(dto, analysis);
- parseSelfConceptFromAnalysis(dto, analysis);
- }
- }
- // 旧字段兼容
- dto.setHeartEmotionStable(dto.getEmotionEmotion() != null ? dto.getEmotionEmotion() : 0);
- dto.setHeartUnderstanding(dto.getBigFiveAgreeableness() != null ? dto.getBigFiveAgreeableness() : 0);
- dto.setHeartLegacy(0);
- }
- /** 行6维 — 家庭主账号版:初期全部回0,后续由自检问卷填充 */
- private void setActionSubDimensionsForParent(MemberEnergyDTO dto) {
- dto.setActionParentChild(0);
- dto.setActionMarital(0);
- dto.setActionParental(0);
- dto.setActionLeadership(0);
- dto.setActionSociality(0);
- dto.setActionInterpersonal(0);
- }
- /** 行6维 — 家庭成员版:初期全部回0,后续由自检问卷填充 */
- private void setActionSubDimensionsForChild(MemberEnergyDTO dto) {
- dto.setActionChildParentChild(0);
- dto.setActionSibling(0);
- dto.setActionRelative(0);
- dto.setActionTeacherStudent(0);
- dto.setActionChildSociality(0);
- dto.setActionChildInterpersonal(0);
- }
- /** 智·成长型思维 — 家庭主账号版:暂无数据,回0 */
- private void setWisdomSubDimensionsForParent(MemberEnergyDTO dto) {
- dto.setWisdomGrowthMindset(0);
- }
- /** 智·成长型思维 — 家庭成员版:从 DAN structuredAnalysis extra.growthMindset 解析 */
- private void setWisdomSubDimensionsForChild(MemberEnergyDTO dto, FamilyMember child) {
- DanAssessmentResult dan = findLatestDanResult(child.getId());
- if (dan != null) {
- Map<String, Object> analysis = parseStructuredAnalysis(dan.getStructuredAnalysis());
- if (analysis != null) {
- Map<String, Object> extra = (Map<String, Object>) analysis.get("extra");
- Map<String, Object> gm = (Map<String, Object>) getNestedMap(extra, "growthMindset");
- if (gm != null) {
- dto.setWisdomGrowthMindset(getIntFromNestedMap(gm, "成长型思维", "growthMindset"));
- }
- }
- }
- }
- /** 富·家族传承 — 家庭主账号版:初期回0,后续由自检问卷填充 */
- private void setWealthSubDimensionsForParent(MemberEnergyDTO dto) {
- dto.setWealthLegacy(0);
- }
- /** 富·家族传承 — 家庭成员版:初期回0,后续由自检问卷填充 */
- private void setWealthSubDimensionsForChild(MemberEnergyDTO dto) {
- dto.setWealthChildLegacy(0);
- }
- /** 从 structuredAnalysis 解析自驱力3维(extra.selfDriving 嵌套 Map,中文 key) */
- @SuppressWarnings("unchecked")
- private void parseSelfDrivingFromAnalysis(MemberEnergyDTO dto, Map<String, Object> analysis) {
- if (analysis == null) return;
- Map<String, Object> extra = (Map<String, Object>) analysis.get("extra");
- if (extra == null) return;
- Map<String, Object> sd = (Map<String, Object>) extra.get("selfDriving");
- if (sd == null) return;
- dto.setDriveAutonomy(getIntFromNestedMap(sd, "自主性", "selfDriving_autonomy"));
- dto.setDriveCompetence(getIntFromNestedMap(sd, "胜任感", "selfDriving_competence"));
- dto.setDriveRelatedness(getIntFromNestedMap(sd, "归属感", "selfDriving_relatedness"));
- }
- /** 从 structuredAnalysis 解析综合自我概念(extra.selfConcept 各维均值) */
- @SuppressWarnings("unchecked")
- private void parseSelfConceptFromAnalysis(MemberEnergyDTO dto, Map<String, Object> analysis) {
- if (analysis == null) return;
- Map<String, Object> extra = (Map<String, Object>) analysis.get("extra");
- if (extra == null) return;
- Map<String, Object> sc = (Map<String, Object>) extra.get("selfConcept");
- if (sc == null || sc.isEmpty()) return;
- // 取所有值的均值作为综合自我概念分
- int sum = 0, count = 0;
- for (Object val : sc.values()) {
- Integer v = parseNumber(val);
- if (v != null) { sum += v; count++; }
- }
- dto.setSelfConceptOverall(count > 0 ? sum / count : 0);
- }
- /** 安全获取嵌套 map */
- @SuppressWarnings("unchecked")
- private Map<String, Object> getNestedMap(Map<String, Object> parent, String key) {
- if (parent == null) return null;
- Object val = parent.get(key);
- return val instanceof Map ? (Map<String, Object>) val : null;
- }
- /** 从嵌套 map 中提取整数分值(优先中文 key,回退英文 key) */
- private Integer getIntFromNestedMap(Map<String, Object> map, String chineseKey, String englishKey) {
- if (map == null) return 0;
- // 先试中文 key
- Object val = map.get(chineseKey);
- if (val == null) val = map.get(englishKey);
- return parseNumber(val);
- }
- /** 将 Object 解析为整数 */
- private Integer parseNumber(Object val) {
- if (val == null) return null;
- if (val instanceof Number) return ((Number) val).intValue();
- if (val instanceof String) {
- try { return Integer.parseInt(((String) val).trim()); } catch (NumberFormatException e) { return null; }
- }
- return null;
- }
- /**
- * 解析 structuredAnalysis JSON(使用 fastjson)。
- * 注意:structured_analysis 实际存储的是 DAN 解析 items 平铺数组
- * [{code,name,value,category},...](见 DanReportUploadService#confirmUpload)。
- * 本方法只消费嵌套对象形态(extra.selfDriving/selfConcept 等),
- * 数组形态无这些数据,静默返回 null 由调用方降级(自驱力/自我概念回 0)。
- */
- @SuppressWarnings("unchecked")
- private Map<String, Object> parseStructuredAnalysis(String json) {
- if (json == null || json.isEmpty()) return null;
- String trimmed = json.trim();
- if (trimmed.startsWith("[")) {
- // 平铺 items 数组(无 extra 嵌套):非异常,静默返回 null
- return null;
- }
- try {
- return JSON.parseObject(trimmed, Map.class);
- } catch (Exception e) {
- log.warn("解析 structuredAnalysis 失败: {}", e.getMessage());
- return null;
- }
- }
- /**
- * 计算指定成员的任务完成率得分 (0-100)
- * Optimized: single query for all categories instead of N queries per category
- *
- * @param memberId executorId (家庭主账号=userId, 家庭成员=memberId)
- * @param memberType 'parent' 或 'child'
- * @param categories Task.category 匹配列表
- * @param daysWindow 统计窗口(天)
- */
- private int calcTaskCompletionRate(Long memberId, String memberType,
- List<String> categories, int daysWindow) {
- Calendar cal = Calendar.getInstance();
- cal.add(Calendar.DAY_OF_YEAR, -daysWindow);
- Date since = cal.getTime();
- // Single query for ALL categories — eliminates N+1
- LambdaQueryWrapper<Task> wrapper = new LambdaQueryWrapper<Task>()
- .eq(Task::getExecutorType, memberType)
- .eq(Task::getExecutorId, memberId)
- .in(Task::getCategory, categories)
- .ne(Task::getIsTemplate, 1)
- .ne(Task::getStatus, "cancelled")
- .gt(Task::getCreatedAt, since);
- SortUtil.applySort(wrapper);
- List<Task> tasks = taskMapper.selectList(wrapper);
- long total = tasks.size();
- long completed = tasks.stream().filter(t -> "completed".equals(t.getStatus())).count();
- if (total == 0) return 0;
- return (int) Math.round((double) completed / total * 100);
- }
- private int safeScore(Integer score) {
- return score != null ? score : 0;
- }
- private boolean hasParentRole(User user) {
- if (user.getRole() != null && "parent".equals(user.getRole())) {
- return true;
- }
- if (user.getRoles() == null || user.getRoles().isEmpty()) {
- return false;
- }
- return user.getRoles().contains("parent");
- }
- private EnergySandboxDTO buildEmptyDTO() {
- EnergySandboxDTO dto = new EnergySandboxDTO();
- dto.setBodyScore(0);
- dto.setMindScore(0);
- dto.setWisdomScore(0);
- dto.setActionScore(0);
- dto.setWealthScore(0);
- dto.setOverallScore(0);
- dto.setMembers(Collections.emptyList());
- return dto;
- }
- // ==================== 五维能量账本系统(新增) ====================
- private static final Map<String, Integer> DAILY_LIMIT_MAP = new LinkedHashMap<>();
- static {
- DAILY_LIMIT_MAP.put("body", 50);
- DAILY_LIMIT_MAP.put("mind", 40);
- DAILY_LIMIT_MAP.put("wisdom", 80);
- DAILY_LIMIT_MAP.put("action", 60);
- DAILY_LIMIT_MAP.put("wealth", 50);
- }
- private static final int GLOBAL_DAILY_LIMIT = 200;
- @Resource
- private EnergyDimensionMapper energyDimensionMapper;
- @Resource
- private EnergySourceConfigMapper energySourceConfigMapper;
- @Resource
- private EnergyBehaviorConfigMapper energyBehaviorConfigMapper;
- @Resource
- private EnergyLogMapper energyLogMapper;
- @Resource
- private EnergyBalanceMapper energyBalanceMapper;
- @Resource
- private WishMapper wishMapper;
- // 维度缓存(懒加载)
- private List<EnergyDimension> dimCache;
- private Map<String, EnergyDimension> dimCodeMap;
- private Map<Long, EnergyDimension> dimIdMap;
- /**
- * 发放能量 — 按 energy_source_config 比例分配至各维度
- *
- * @param memberId 家庭成员ID
- * @param sourceType 来源类型 (task/product/medical_report/etc)
- * @param sourceId 来源ID
- * @param totalAmount 总能量值
- * @param description 描述
- * @param daysToExpire 过期天数(null=不过期)
- * @return Map<dimensionCode, amount>
- */
- @Transactional
- public Map<String, Integer> awardEnergy(Long memberId, String sourceType, Long sourceId,
- Integer totalAmount, String description,
- Integer daysToExpire) {
- if (totalAmount == null || totalAmount <= 0) return new HashMap<>();
- // 0. 查 behavior_config:sourceType 映射到 behavior_code
- EnergyBehaviorConfig behaviorConfig = matchBehaviorConfig(sourceType);
- if (behaviorConfig != null) {
- return awardByConfig(memberId, behaviorConfig, sourceType, sourceId, totalAmount, description, daysToExpire);
- }
- // 1. 无 behavior_config 时使用旧逻辑:查 energy_source_config → Fallback
- List<EnergySourceConfig> configs = energySourceConfigMapper.selectList(
- new LambdaQueryWrapper<EnergySourceConfig>()
- .eq(EnergySourceConfig::getSourceType, sourceType)
- .eq(EnergySourceConfig::getSourceId, sourceId)
- );
- Map<Long, Integer> dimRatios = resolveDimensionRatios(configs, sourceType, sourceId);
- if (dimRatios.isEmpty()) return new HashMap<>();
- // 2. 日上限检查
- if (isDailyLimitExceeded(memberId, dimRatios.keySet(), totalAmount)) {
- log.warn("日上限已达,跳过发放: memberId={}, amount={}", memberId, totalAmount);
- return new HashMap<>();
- }
- // 3. 按比例分配
- Map<Long, Integer> allocations = calculateAllocations(totalAmount, dimRatios);
- // 4. 更新余额 + 写流水
- return executeAward(memberId, sourceType, sourceId, totalAmount, description, daysToExpire, allocations);
- }
- /**
- * 根据 behavior_config 发放能量
- */
- @Transactional
- public Map<String, Integer> awardByConfig(Long memberId, 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(memberId, dimRatios.keySet(), totalAmount, configDailyLimit)) {
- log.warn("行为日上限已达,跳过: behaviorCode={}, memberId={}", config.getBehaviorCode(), memberId);
- return new HashMap<>();
- }
- } else {
- if (isDailyLimitExceeded(memberId, dimRatios.keySet(), totalAmount)) {
- log.warn("全局日上限已达,跳过: behaviorCode={}, memberId={}", config.getBehaviorCode(), memberId);
- 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(memberId, sourceType, sourceId, totalAmount, description, expireDays, allocations);
- }
- /**
- * 执行能量发放(更新余额+写流水)
- */
- private Map<String, Integer> executeAward(Long memberId, String sourceType, Long sourceId,
- Integer totalAmount, String description,
- Integer expireDays, Map<Long, Integer> allocations) {
- Map<String, Integer> result = new LinkedHashMap<>();
- Date now = new Date();
- Date expiresAt = expireDays != null
- ? new Date(now.getTime() + (long) expireDays * 86400000L) : null;
- for (Map.Entry<Long, Integer> entry : allocations.entrySet()) {
- if (entry.getValue() <= 0) continue;
- EnergyBalance balance = getOrCreateBalance(memberId, entry.getKey());
- balance.setBalance(balance.getBalance() + entry.getValue());
- balance.setTotalEarned(balance.getTotalEarned() + entry.getValue());
- balance.setUpdatedAt(now);
- energyBalanceMapper.updateById(balance);
- EnergyLog elog = new EnergyLog();
- elog.setMemberId(memberId);
- elog.setDimensionId(entry.getKey());
- elog.setAmount(entry.getValue());
- elog.setBalanceAfter(balance.getBalance());
- elog.setSourceType(sourceType);
- elog.setSourceId(sourceId);
- elog.setExpiresAt(expiresAt);
- elog.setDescription(description);
- elog.setCreatedAt(now);
- energyLogMapper.insert(elog);
- EnergyDimension dim = getDimById(entry.getKey());
- if (dim != null) result.put(dim.getCode(), entry.getValue());
- }
- // 发送能量变更通知
- try { sendEnergyNotification(memberId, sourceType, totalAmount, description); } catch (Exception e) { log.warn("energy change notification failed", e); }
- 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.getIntValue("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;
- }
- /**
- * 扣除能量
- *
- * @return 扣除后余额,-1 = 余额不足
- */
- @Transactional
- public int deductEnergy(Long memberId, Long dimensionId, Integer amount, String reason) {
- if (amount == null || amount <= 0)
- throw new IllegalArgumentException("扣除数量必须为正数");
- EnergyBalance balance = getOrCreateBalance(memberId, dimensionId);
- if (balance.getBalance() < amount) return -1;
- balance.setBalance(balance.getBalance() - amount);
- balance.setTotalSpent(balance.getTotalSpent() + amount);
- balance.setUpdatedAt(new Date());
- energyBalanceMapper.updateById(balance);
- EnergyLog elog = new EnergyLog();
- elog.setMemberId(memberId);
- elog.setDimensionId(dimensionId);
- elog.setAmount(-amount);
- elog.setBalanceAfter(balance.getBalance());
- elog.setDescription(reason);
- elog.setCreatedAt(new Date());
- energyLogMapper.insert(elog);
- // 发送能量扣除通知
- try { sendEnergyDeductionNotification(memberId, "manual", amount, reason); } catch (Exception e) { log.warn("energy deduction notification failed", e); }
- return balance.getBalance();
- }
- /**
- * 扣除能量 — 通过维度code(自动查ID)
- *
- * @return 扣除后余额,-1 = 余额不足
- */
- @Transactional
- public int deductEnergyByCode(Long memberId, String dimensionCode, Integer amount, String reason) {
- EnergyDimension dim = getDimByCode(dimensionCode);
- if (dim == null) {
- log.warn("EnergyService.deductEnergyByCode: dimension not found, code={}", dimensionCode);
- return -1;
- }
- return deductEnergy(memberId, dim.getId(), amount, reason);
- }
- /**
- * 发放能量 — 通过维度code(自动查ID),无日上限/配置约束,供低频行为直接发放
- *
- * @return 发放结果 Map<dimensionCode, amount>(空 = 维度不存在未发放)
- */
- @Transactional
- public Map<String, Integer> awardEnergyByCode(Long memberId, String dimensionCode, Integer amount, String reason) {
- if (amount == null || amount <= 0) return new HashMap<>();
- EnergyDimension dim = getDimByCode(dimensionCode);
- if (dim == null) {
- log.warn("EnergyService.awardEnergyByCode: dimension not found, code={}", dimensionCode);
- return new HashMap<>();
- }
- Map<Long, Integer> allocations = new LinkedHashMap<>();
- allocations.put(dim.getId(), amount);
- return executeAward(memberId, "challenge", null, amount, reason, null, allocations);
- }
- /**
- * 查询五维能量概览
- */
- public Map<String, Object> getOverview(Long memberId) {
- List<EnergyDimension> allDims = getAllDimensions();
- List<Map<String, Object>> dimList = new ArrayList<>();
- int totalEnergy = 0;
- for (EnergyDimension dim : allDims) {
- if (dim.getStatus() != 1) continue;
- EnergyBalance balance = energyBalanceMapper.selectOne(
- new LambdaQueryWrapper<EnergyBalance>()
- .eq(EnergyBalance::getMemberId, memberId)
- .eq(EnergyBalance::getDimensionId, dim.getId())
- );
- int energy = balance != null ? balance.getBalance() : 0;
- totalEnergy += energy;
- Map<String, Object> item = new LinkedHashMap<>();
- item.put("code", dim.getCode());
- item.put("name", dim.getName());
- item.put("icon", dim.getIcon());
- item.put("element", dim.getElement());
- item.put("energy", energy);
- item.put("healthIndex", 0); // 算法待定
- dimList.add(item);
- }
- Map<String, Object> result = new LinkedHashMap<>();
- result.put("dimensions", dimList);
- result.put("totalEnergy", totalEnergy);
- result.put("totalHealthIndex", 0);
- return result;
- }
- /**
- * 流水查询(按维度筛选,分页)
- */
- public Page<EnergyLog> getLogs(Long memberId, String dimensionCode, Integer page, Integer size) {
- if (page == null || page < 1) page = 1;
- if (size == null || size < 1) size = 10;
- Page<EnergyLog> pageParam = new Page<>(page, size);
- LambdaQueryWrapper<EnergyLog> wrapper = new LambdaQueryWrapper<EnergyLog>()
- .eq(EnergyLog::getMemberId, memberId);
- if (dimensionCode != null && !dimensionCode.isEmpty()) {
- EnergyDimension dim = getDimByCode(dimensionCode);
- if (dim != null) wrapper.eq(EnergyLog::getDimensionId, dim.getId());
- }
- wrapper.orderByDesc(EnergyLog::getCreatedAt);
- SortUtil.applySort(wrapper);
- return energyLogMapper.selectPage(pageParam, wrapper);
- }
- /**
- * 配置来源比例
- */
- @Transactional
- public void configureSourceRatios(String sourceType, Long sourceId, String sourceName,
- List<Map<String, Object>> ratios) {
- energySourceConfigMapper.delete(
- new LambdaQueryWrapper<EnergySourceConfig>()
- .eq(EnergySourceConfig::getSourceType, sourceType)
- .eq(EnergySourceConfig::getSourceId, sourceId)
- );
- for (Map<String, Object> r : ratios) {
- String dimCode = (String) r.get("dimensionCode");
- Integer ratioVal = Integer.valueOf(r.get("ratio").toString());
- EnergyDimension dim = getDimByCode(dimCode);
- if (dim == null) continue;
- EnergySourceConfig config = new EnergySourceConfig();
- config.setSourceType(sourceType);
- config.setSourceId(sourceId);
- config.setSourceName(sourceName);
- config.setDimensionId(dim.getId());
- config.setRatio(ratioVal);
- config.setCreatedAt(new Date());
- energySourceConfigMapper.insert(config);
- }
- }
- /**
- * 发放能量 - 任务奖励
- *
- * @param memberId 家庭成员ID
- * @param taskId 任务ID
- * @param amount 能量数量
- * @param dimensionCode 维度代码
- * @return EnergyLog 能量日志
- */
- @Transactional
- public EnergyLog grantEnergy(Long memberId, Long taskId, Long amount, String dimensionCode) {
- // CFC-EN-001 修复:grantEnergy 的维度由调用方显式指定,能量应直接发放到该维度。
- // 旧实现委托 awardEnergy("task"),命中 task_complete 行为配置(100% 分配到 action),
- // 随后按请求的 dimensionCode 从分配结果取数,请求 body 维度时 balanceMap.get("body")=null
- // → 抛 RuntimeException → GlobalExceptionHandler 未映射 → code=500「服务器内部错误」。
- if (amount == null || amount <= 0) {
- throw new IllegalArgumentException("能量数量必须为正数");
- }
- EnergyDimension dim = getDimByCode(dimensionCode);
- if (dim == null) {
- throw new IllegalArgumentException("维度代码无效:" + dimensionCode);
- }
- Map<Long, Integer> allocations = new LinkedHashMap<>();
- allocations.put(dim.getId(), amount.intValue());
- // 日上限检查(沿用全局/维度上限逻辑,超限改为 400 业务提示而非 500)
- if (isDailyLimitExceeded(memberId, allocations.keySet(), amount.intValue())) {
- log.warn("日上限已达,跳过发放: memberId={}, dimensionCode={}, amount={}",
- memberId, dimensionCode, amount);
- throw new IllegalStateException("今日能量发放已达上限,请明天再试");
- }
- // 执行发放(更新余额 + 写流水 + 站内通知)
- Map<String, Integer> balanceMap = executeAward(memberId, "task", taskId,
- amount.intValue(), "任务奖励", null, allocations);
- Integer energyAmount = balanceMap.get(dimensionCode);
- if (energyAmount == null || energyAmount <= 0) {
- throw new IllegalStateException("能量发放失败:维度 " + dimensionCode + " 没有分配能量");
- }
- // 构建返回对象(executeAward 已写入日志,此处不再重复插入)
- EnergyLog energyLog = new EnergyLog();
- energyLog.setMemberId(memberId);
- energyLog.setDimensionId(dim.getId());
- energyLog.setAmount(energyAmount);
- energyLog.setSourceType("task");
- energyLog.setSourceId(taskId);
- energyLog.setDescription("任务奖励");
- energyLog.setCreatedAt(new Date());
- return energyLog;
- }
- /**
- * 扣除能量 - 用于心愿兑换
- *
- * @param memberId 家庭成员ID
- * @param wishId 心愿ID
- * @param amount 扣除能量数量
- * @return Boolean 是否成功
- */
- @Transactional
- public Boolean deductForWish(Long memberId, Long wishId, Long amount) {
- // 使用 deductEnergyByCode 扣除能量,维度为 "action"
- int result = deductEnergyByCode(memberId, "action", amount.intValue(), "心愿兑换");
-
- // 返回 true 表示成功,false 表示余额不足
- return result != -1;
- }
- /**
- * 发送能量变更站内通知
- */
- private void sendEnergyNotification(Long memberId, String sourceType, Integer amount, String description) {
- FamilyMember member = familyMemberMapper.selectById(memberId);
- Long userId = member != null ? member.getUserId() : null;
- if (userId == null) return;
- String title = "";
- String content = "";
- switch (sourceType) {
- case "task":
- title = "任务完成获得能量";
- content = String.format("完成了%s获得%d能量", description != null ? description : "任务", amount);
- break;
- case "product":
- title = "购买商品扣除能量";
- content = String.format("购买了%s扣除%d能量", description != null ? description : "商品", Math.abs(amount));
- break;
- case "medical_report":
- title = "上传体检报告获得能量";
- content = String.format("上传了%s获得%d能量", description != null ? description : "体检报告", amount);
- break;
- case "daily_record":
- title = "日常记录获得能量";
- content = String.format("完成了日常记录获得%d能量", amount);
- break;
- default:
- title = "能量变动";
- content = String.format("能量变动%d", amount);
- }
- Map<String, Object> vars = new HashMap<>();
- vars.put("amount", (amount > 0 ? "+" : "") + String.valueOf(amount));
- vars.put("source", sourceType);
- vars.put("description", description);
- notificationDispatchService.sendByTemplate("energy_change", userId, vars, "energyLog", memberId, title, content);
- }
- /**
- * 发送能量扣除站内通知
- */
- private void sendEnergyDeductionNotification(Long memberId, String sourceType, Integer amount, String description) {
- FamilyMember member = familyMemberMapper.selectById(memberId);
- Long userId = member != null ? member.getUserId() : null;
- if (userId == null) return;
- String title = "";
- String content = "";
- switch (sourceType) {
- case "task":
- title = "任务完成获得能量";
- content = String.format("完成了%s获得%d能量", description != null ? description : "任务", amount);
- break;
- case "wish":
- title = "心愿兑换扣除能量";
- content = String.format("完成了心愿兑换扣除%d能量", Math.abs(amount));
- break;
- default:
- title = "能量扣除";
- content = String.format("能量扣除%d", Math.abs(amount));
- }
- Map<String, Object> vars = new HashMap<>();
- vars.put("amount", (amount > 0 ? "+" : "") + String.valueOf(amount));
- vars.put("source", sourceType);
- vars.put("description", description);
- notificationDispatchService.sendByTemplate("energy_deduction", userId, vars, "energyLog", memberId, title, content);
- }
- // ==================== 辅助方法 ====================
- /**
- * 解析维度比例配置,按Fallback链降级
- */
- private Map<Long, Integer> resolveDimensionRatios(
- List<EnergySourceConfig> configs, String sourceType, Long sourceId) {
- if (!configs.isEmpty()) {
- Map<Long, Integer> result = new LinkedHashMap<>();
- for (EnergySourceConfig c : configs) result.put(c.getDimensionId(), c.getRatio());
- return result;
- }
- // Fallback: 默认 → 行(action) 100%
- EnergyDimension action = getDimByCode("action");
- if (action != null) return Collections.singletonMap(action.getId(), 10000);
- return Collections.emptyMap();
- }
- /**
- * 按比例分配整数,余数加到最大比例维度
- */
- private Map<Long, Integer> calculateAllocations(Integer total, Map<Long, Integer> ratios) {
- int totalRatio = ratios.values().stream().mapToInt(Integer::intValue).sum();
- if (totalRatio == 0) return Collections.emptyMap();
- Map<Long, Integer> result = new LinkedHashMap<>();
- int allocated = 0;
- // 第一轮:按比例分配(向下取整)
- Long maxRatioDim = null;
- int maxRatio = 0;
- for (Map.Entry<Long, Integer> entry : ratios.entrySet()) {
- int val = total * entry.getValue() / totalRatio;
- result.put(entry.getKey(), val);
- allocated += val;
- if (entry.getValue() > maxRatio) {
- maxRatio = entry.getValue();
- maxRatioDim = entry.getKey();
- }
- }
- // 第二轮:余数加到最大比例维度
- int remainder = total - allocated;
- if (remainder > 0 && maxRatioDim != null) {
- result.put(maxRatioDim, result.get(maxRatioDim) + remainder);
- }
- return result;
- }
- /**
- * 获取或创建维度余额记录
- */
- private EnergyBalance getOrCreateBalance(Long memberId, Long dimId) {
- EnergyBalance balance = energyBalanceMapper.selectOne(
- new LambdaQueryWrapper<EnergyBalance>()
- .eq(EnergyBalance::getMemberId, memberId)
- .eq(EnergyBalance::getDimensionId, dimId)
- );
- if (balance == null) {
- balance = new EnergyBalance();
- balance.setMemberId(memberId);
- balance.setDimensionId(dimId);
- balance.setBalance(0);
- balance.setTotalEarned(0);
- balance.setTotalSpent(0);
- balance.setUpdatedAt(new Date());
- energyBalanceMapper.insert(balance);
- }
- return balance;
- }
- /**
- * 检查日上限是否已达
- */
- private boolean isDailyLimitExceeded(Long memberId, Set<Long> dimIds, Integer amount) {
- return isDailyLimitExceeded(memberId, dimIds, amount, GLOBAL_DAILY_LIMIT);
- }
- /**
- * 检查日上限(支持自定义全局上限)
- */
- private boolean isDailyLimitExceeded(Long memberId, Set<Long> dimIds, Integer amount, Integer customGlobalLimit) {
- if (amount == null || amount <= 0) return true;
- Date todayStart = getTodayStart();
- Date todayEnd = getTodayEnd();
- // 全局日上限(使用自定义上限)
- LambdaQueryWrapper<EnergyLog> globalWrapper = new LambdaQueryWrapper<EnergyLog>()
- .eq(EnergyLog::getMemberId, memberId)
- .gt(EnergyLog::getAmount, 0)
- .between(EnergyLog::getCreatedAt, todayStart, todayEnd);
- Integer globalToday = energyLogMapper.selectList(globalWrapper).stream()
- .mapToInt(e -> e.getAmount() != null ? e.getAmount() : 0)
- .sum();
- int globalLimit = customGlobalLimit != null ? customGlobalLimit : GLOBAL_DAILY_LIMIT;
- if (globalToday + amount > globalLimit) return true;
- // 各维度日上限
- for (Long dimId : dimIds) {
- LambdaQueryWrapper<EnergyLog> dimWrapper = new LambdaQueryWrapper<EnergyLog>()
- .eq(EnergyLog::getMemberId, memberId)
- .eq(EnergyLog::getDimensionId, dimId)
- .gt(EnergyLog::getAmount, 0)
- .between(EnergyLog::getCreatedAt, todayStart, todayEnd);
- Integer dimToday = energyLogMapper.selectList(dimWrapper).stream()
- .mapToInt(e -> e.getAmount() != null ? e.getAmount() : 0)
- .sum();
- EnergyDimension dim = getDimById(dimId);
- String code = dim != null ? dim.getCode() : "";
- Integer limit = DAILY_LIMIT_MAP.getOrDefault(code, 50);
- if (dimToday + amount > limit) return true;
- }
- return false;
- }
- /**
- * 获取今日起始时间(00:00:00)
- */
- private Date getTodayStart() {
- Calendar cal = Calendar.getInstance();
- cal.set(Calendar.HOUR_OF_DAY, 0);
- cal.set(Calendar.MINUTE, 0);
- cal.set(Calendar.SECOND, 0);
- cal.set(Calendar.MILLISECOND, 0);
- return cal.getTime();
- }
- /**
- * 获取今日结束时间(23:59:59)
- */
- private Date getTodayEnd() {
- Calendar cal = Calendar.getInstance();
- cal.set(Calendar.HOUR_OF_DAY, 23);
- cal.set(Calendar.MINUTE, 59);
- cal.set(Calendar.SECOND, 59);
- cal.set(Calendar.MILLISECOND, 999);
- return cal.getTime();
- }
- /**
- * 获取所有维度(带缓存)
- */
- private List<EnergyDimension> getAllDimensions() {
- if (dimCache == null) {
- dimCache = energyDimensionMapper.selectList(
- new LambdaQueryWrapper<EnergyDimension>()
- .orderByAsc(EnergyDimension::getSortOrder)
- );
- // 统一维度体系:五维五行归属改以注册中心为准(root 节点 wuxing_rule_ref,与旧
- // energy_dimension.element 同值,迁移448 已对齐)。注册中心未就绪或维度缺失时
- // 保留旧表值,保证五行回归结果不变、返回结构不变。
- enrichWuxingElementFromRegistry(dimCache);
- }
- return dimCache;
- }
- /**
- * 统一维度体系:按注册中心 5 根节点的 wuxing_rule_ref 覆盖五行元素归属(element)。
- *
- * <p>维度 code(body/mind/wisdom/action/wealth)与统一 5 根同码,无需 alias 翻译;
- * id/name/icon/sortOrder/status 仍以 energy_dimension 表为准(energy_log/energy_balance
- * 以 dimension_id 关联该表 id,切换仅限五行归属来源,保证回归不变)。</p>
- *
- * @param dims 已加载的旧表维度列表(就地更新 element)
- */
- private void enrichWuxingElementFromRegistry(List<EnergyDimension> dims) {
- if (dims == null || dims.isEmpty() || dimensionRegistryService == null) {
- return;
- }
- try {
- List<DimensionNode> roots = dimensionRegistryService.getRoots();
- if (roots == null || roots.isEmpty()) {
- return;
- }
- Map<String, String> elementByCode = new HashMap<>();
- for (DimensionNode root : roots) {
- if (root == null || root.getCode() == null || root.getWuxingRuleRef() == null) {
- continue;
- }
- elementByCode.put(root.getCode(), root.getWuxingRuleRef());
- }
- for (EnergyDimension d : dims) {
- if (d == null || d.getCode() == null) {
- continue;
- }
- String element = elementByCode.get(d.getCode());
- if (element != null) {
- d.setElement(element);
- }
- }
- } catch (Exception e) {
- log.warn("EnergyService.enrichWuxingElementFromRegistry: 注册中心五行归属补齐失败(忽略): {}", e.getMessage());
- }
- }
- /**
- * 按code查维度
- */
- private EnergyDimension getDimByCode(String code) {
- initDimMaps();
- return dimCodeMap.get(code);
- }
- /**
- * 按ID查维度
- */
- private EnergyDimension getDimById(Long id) {
- initDimMaps();
- return dimIdMap.get(id);
- }
- private void initDimMaps() {
- if (dimCodeMap == null || dimIdMap == null) {
- List<EnergyDimension> all = getAllDimensions();
- dimCodeMap = all.stream().collect(Collectors.toMap(EnergyDimension::getCode, d -> d));
- dimIdMap = all.stream().collect(Collectors.toMap(EnergyDimension::getId, d -> d));
- }
- }
- }
|