| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994 |
- 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.common.Result;
- import com.etotem.cfc.dto.CreateProductOrderDTO;
- import com.etotem.cfc.dto.CreateTaskDTO;
- import com.etotem.cfc.dto.ProductOrderDTO;
- import com.etotem.cfc.dto.TaskReviewDTO;
- import com.etotem.cfc.entity.*;
- import com.etotem.cfc.mapper.*;
- import com.etotem.cfc.service.api.TaskServiceInterface;
- import com.alibaba.fastjson.JSON;
- import com.alibaba.fastjson.JSONObject;
- import com.etotem.cfc.service.OrchestrationEngine;
- import javax.annotation.Resource;
- import lombok.extern.slf4j.Slf4j;
- import org.springframework.context.annotation.Lazy;
- import org.springframework.stereotype.Service;
- import org.springframework.transaction.annotation.Transactional;
- import java.util.*;
- import com.etotem.cfc.util.DimensionCodes;
- import com.etotem.cfc.util.SortUtil;
- import com.etotem.cfc.service.NotificationDispatchService;
- @Slf4j
- @Service
- public class TaskService implements TaskServiceInterface {
- @Resource
- private TaskMapper taskMapper;
- @Resource
- private NotificationDispatchService notificationDispatchService;
- @Resource
- private TaskReminderConfigService taskReminderConfigService;
- @Resource
- private TaskExecutionMapper taskExecutionMapper;
- @Resource
- private FamilyMemberMapper familyMemberMapper;
- @Resource
- private UserMapper userMapper;
- @Resource
- private PointsLogMapper pointsLogMapper;
- @Resource
- @Lazy
- private UserService userService;
- @Resource
- private StreakService streakService;
- @Resource
- private MiniGameService miniGameService;
- @Resource
- private MiniGameMapper miniGameMapper;
- @Resource
- private EnergyService energyService;
- @Resource
- private FamilyRelationshipScoreMapper familyRelationshipScoreMapper;
- @Resource
- private CartService cartService;
- @Resource
- private ProductOrderService productOrderService;
- @Resource
- private ConsigneeMapper consigneeMapper;
- @Resource
- private SurveyService surveyService;
- @Resource
- private SurveyTemplateMapper surveyTemplateMapper;
- @Resource
- @Lazy
- private OrchestrationEngine orchestrationEngine;
- @Resource
- private TaskTemplateMapper taskTemplateMapper;
- @Resource
- private PlatformPointsService platformPointsService;
- @Resource
- private FamilyPlatformPointsService familyPlatformPointsService;
- private static final int EARLY_BONUS = 1; // 提前完成奖励
- private static final int PENALTY_MAX_DAILY = 5; // 每日最多扣分
- private static final int LATE_GRACE_MINUTES = 10; // 10分钟内不算迟到
- @Transactional
- public Long createTask(Long userId, CreateTaskDTO dto) {
- User currentUser = userService.getUserInfo(userId);
-
- // 处理执行者类型 - 支持孩子任务、家长任务、家庭成员任务
- String executorType = dto.getExecutorType();
- if (executorType == null || executorType.isEmpty()) {
- executorType = "child";
- }
- if ("member".equals(executorType)) {
- Long currentMemberId = getCurrentMemberId(userId);
- long familyMemberCount = familyMemberMapper.selectCount(
- new LambdaQueryWrapper<FamilyMember>()
- .eq(FamilyMember::getFamilyId, currentUser.getFamilyId()));
- if (familyMemberCount < 2) {
- throw new IllegalArgumentException("该任务需分配给其他家庭成员,当前家庭不足2人");
- }
- List<Long> assigneeIds = dto.getMemberIds();
- if (assigneeIds == null || assigneeIds.isEmpty()) {
- if (dto.getExecutorId() != null) {
- assigneeIds = Collections.singletonList(dto.getExecutorId());
- } else if (dto.getMemberId() != null) {
- assigneeIds = Collections.singletonList(dto.getMemberId());
- } else {
- assigneeIds = Collections.emptyList();
- }
- }
- for (Long assigneeId : assigneeIds) {
- if (assigneeId == null) continue;
- if (assigneeId.equals(currentMemberId)) {
- throw new IllegalArgumentException("不能给自己分配家庭成员任务");
- }
- Long count = familyMemberMapper.selectCount(
- new LambdaQueryWrapper<FamilyMember>()
- .eq(FamilyMember::getId, assigneeId)
- .eq(FamilyMember::getFamilyId, currentUser.getFamilyId()));
- if (count == 0) {
- throw new IllegalArgumentException("被分配成员不属于本家庭");
- }
- }
- }
- // 支持多孩子:为每个孩子创建独立任务实例
- List<Long> childIds = dto.getMemberIds();
- if (childIds == null || childIds.isEmpty()) {
- // 兼容旧版:使用单个childId
- if (dto.getMemberId() != null) {
- childIds = Collections.singletonList(dto.getMemberId());
- } else if (dto.getExecutorId() != null && "child".equals(executorType)) {
- childIds = Collections.singletonList(dto.getExecutorId());
- } else {
- childIds = Collections.emptyList();
- }
- }
-
- // 如果有多个孩子,为每个孩子创建任务
- if (!childIds.isEmpty() && childIds.size() > 1) {
- Long firstChildId = childIds.get(0);
- Task task = createSingleTask(currentUser, userId, executorType, firstChildId, dto);
- taskMapper.insert(task);
- ensureSurveyTemplateForTask(task);
- // 为其他孩子创建任务实例
- for (int i = 1; i < childIds.size(); i++) {
- Task additionalTask = createSingleTask(currentUser, userId, executorType, childIds.get(i), dto);
- taskMapper.insert(additionalTask);
- ensureSurveyTemplateForTask(additionalTask);
- }
- return task.getId();
- } else {
- // 单个孩子或无孩子
- Long memberId = childIds.isEmpty() ? null : childIds.get(0);
- Task task = createSingleTask(currentUser, userId, executorType, memberId, dto);
- taskMapper.insert(task);
- ensureSurveyTemplateForTask(task);
- return task.getId();
- }
- }
-
- /**
- * 创建单个任务实例
- */
- private Task createSingleTask(User currentUser, Long userId, String executorType, Long memberId, CreateTaskDTO dto) {
- Task task = new Task();
- task.setFamilyId(currentUser.getFamilyId());
- task.setCreatorId(userId);
- task.setExecutorType(executorType);
-
- if ("child".equals(executorType)) {
- task.setExecutorId(memberId);
- task.setFamilyMemberId(memberId);
- task.setChildId(memberId);
- } else if ("member".equals(executorType)) {
- task.setExecutorId(memberId);
- task.setChildId(null);
- task.setStatus("assigned");
- task.setAcceptDeadline(dto.getDeadline());
- } else {
- task.setExecutorId(dto.getExecutorId() != null ? dto.getExecutorId() : userId);
- task.setChildId(null);
- }
-
- task.setTitle(dto.getTitle());
- task.setDescription(dto.getDescription());
- task.setPoints(dto.getPoints() != null ? dto.getPoints() : 2);
- task.setDuration(dto.getDuration());
- // 处理小游戏任务
- task.setMinigameCode(dto.getMinigameCode());
- if ("小游戏类".equals(dto.getCategory()) && dto.getMinigameCode() != null) {
- // 小游戏任务:从MiniGame获取默认积分
- MiniGame miniGame = miniGameMapper.selectOne(
- new LambdaQueryWrapper<MiniGame>()
- .eq(MiniGame::getGameCode, dto.getMinigameCode())
- .eq(MiniGame::getStatus, 1)
- );
- if (miniGame != null) {
- if (dto.getPoints() == null) {
- task.setPoints(miniGame.getDefaultPoints() != null ? miniGame.getDefaultPoints() : 5);
- }
- if (dto.getTitle() == null || dto.getTitle().isEmpty()) {
- task.setTitle(miniGame.getGameName());
- }
- }
- }
- // 如果没有指定截止时间,默认今天23:59
- if (dto.getDeadline() == null) {
- Calendar cal = Calendar.getInstance();
- cal.set(Calendar.HOUR_OF_DAY, 23);
- cal.set(Calendar.MINUTE, 59);
- cal.set(Calendar.SECOND, 59);
- task.setDeadline(cal.getTime());
- } else {
- task.setDeadline(dto.getDeadline());
- }
-
- task.setRepeatType(dto.getRepeatType() != null ? dto.getRepeatType() : "none");
- task.setCategory(dto.getCategory());
- // 任务性质
- task.setTaskType(dto.getTaskType());
- task.setFrequency(dto.getFrequency());
- task.setMaxFrequency(dto.getMaxFrequency());
- // 完成方式(转为JSON存储)
- if (dto.getCompleteTypes() != null && !dto.getCompleteTypes().isEmpty()) {
- task.setCompleteTypes(String.join(",", dto.getCompleteTypes()));
- }
- // 审核设置
- task.setReviewType(dto.getReviewType());
-
- // 时间设置
- task.setEarliestStart(dto.getEarliestStart());
- task.setLatestEnd(dto.getLatestEnd());
- // 如果是重复任务,标记为模板
- if (!"none".equals(task.getRepeatType())) {
- task.setIsTemplate(1); // 模板任务
- } else {
- task.setIsTemplate(0);
- }
- // 家长给孩子安排任务需要审核,孩子给家长安排任务也需要审核
- if ("child".equals(executorType)) {
- task.setNeedReview(dto.getNeedReview() != null ? dto.getNeedReview() : 0);
- } else {
- // 孩子给家长安排任务,默认需要审核
- task.setNeedReview(dto.getNeedReview() != null ? dto.getNeedReview() : 1);
- }
- task.setReviewByCategory(dto.getReviewByCategory() != null ? dto.getReviewByCategory() : 0);
- if (!"assigned".equals(task.getStatus())) {
- // TASK-SYS-248: 有前置任务且前置未完成 → 初始状态 locked
- if (dto.getPrerequisiteTaskId() != null) {
- Task prereq = taskMapper.selectById(dto.getPrerequisiteTaskId());
- if (prereq == null || !"completed".equals(prereq.getStatus())) {
- task.setStatus("locked");
- } else {
- task.setStatus("pending");
- }
- } else {
- task.setStatus("pending");
- }
- }
- task.setCreatedAt(new Date());
- task.setUpdatedAt(new Date());
- // 五维维度
- task.setDimensionCode(dto.getDimensionCode());
- task.setDimensionWeights(dto.getDimensionWeights());
- // B20: 会员任务标记透传(自建任务 memberOnly=0,不校验会员)
- task.setMemberOnly(dto.getMemberOnly() != null ? dto.getMemberOnly() : 0);
- // TASK-SYS-248: 行为调度中心扩展字段透传
- task.setPrerequisiteTaskId(dto.getPrerequisiteTaskId());
- task.setActionType(dto.getActionType());
- task.setActionConfig(dto.getActionConfig());
- task.setRequireInput(dto.getRequireInput() != null ? dto.getRequireInput() : 0);
- task.setStartRequired(dto.getStartRequired() != null ? dto.getStartRequired() : 0);
- task.setStartCount(0);
- task.setMinDurationSeconds(dto.getMinDurationSeconds() != null ? dto.getMinDurationSeconds() : 0);
- // TASK-SYS-248: 自动计算是否为流程任务
- // 有前置任务 或 有后置任务 → 标记为流程任务
- if (dto.getPrerequisiteTaskId() != null
- || (dto.getSuccessorTaskIds() != null && !dto.getSuccessorTaskIds().isEmpty())) {
- task.setIsFlowTask(1);
- } else {
- task.setIsFlowTask(0);
- }
- task.setSuccessorTaskIds(dto.getSuccessorTaskIds());
- task.setSourceType(dto.getSourceType() != null ? dto.getSourceType() : "self");
- task.setSourceId(dto.getSourceId());
- task.setIsDailyProgress(dto.getIsDailyProgress() != null ? dto.getIsDailyProgress() : 0);
- task.setTargetValue(dto.getTargetValue());
- // 进度型任务无 deadline(原 growth_task DAILY:每日重置,无截止时间)
- if (task.getIsDailyProgress() != null && task.getIsDailyProgress() == 1) {
- task.setDeadline(null);
- }
- return task;
- }
- private Long getCurrentMemberId(Long userId) {
- FamilyMember member = familyMemberMapper.selectOne(
- new LambdaQueryWrapper<FamilyMember>()
- .eq(FamilyMember::getUserId, userId)
- .last("LIMIT 1"));
- return member != null ? member.getId() : null;
- }
- /**
- * 实现接口:获取今日任务列表(按childId,不过滤维度)
- */
- @Override
- public List<Task> getTodayTasks(Long memberId) {
- return getTodayTasks(memberId, null);
- }
- public List<Task> getTodayTasks(Long memberId, String dimensionCode) {
- Calendar calendar = Calendar.getInstance();
- calendar.set(Calendar.HOUR_OF_DAY, 0);
- calendar.set(Calendar.MINUTE, 0);
- calendar.set(Calendar.SECOND, 0);
- calendar.set(Calendar.MILLISECOND, 0);
- Date startOfDay = calendar.getTime();
- calendar.add(Calendar.DAY_OF_MONTH, 1);
- Date endOfDay = calendar.getTime();
- LambdaQueryWrapper<Task> wrapper = new LambdaQueryWrapper<Task>()
- .eq(Task::getFamilyMemberId, memberId)
- .in(Task::getStatus, Arrays.asList("pending", "completed", "locked", "pending_review"))
- .and(w -> w
- // 普通任务:按 deadline 判断今日
- .and(w2 -> w2.isNotNull(Task::getDeadline)
- .ge(Task::getDeadline, startOfDay)
- .lt(Task::getDeadline, endOfDay))
- // 进度型任务:按 createdAt 判断今日(deadline 为 NULL)
- .or(w3 -> w3.eq(Task::getIsDailyProgress, 1)
- .ge(Task::getCreatedAt, startOfDay)
- .lt(Task::getCreatedAt, endOfDay)))
- .orderByAsc(Task::getDeadline);
- if (dimensionCode != null && !dimensionCode.isEmpty() && DimensionCodes.isValid(dimensionCode)) {
- String dim = dimensionCode.trim();
- wrapper.and(w -> w
- .apply("COALESCE(JSON_EXTRACT(dimension_weights, CONCAT('$.', {0})), 0) > 0", dim)
- .or().eq(Task::getDimensionCode, dim)
- );
- }
- SortUtil.applySort(wrapper);
- List<Task> all = taskMapper.selectList(wrapper);
- fillProgress(all, memberId);
- return filterLockedChildren(all);
- }
- /**
- * 家庭维度今日任务(家长端总览:方案分解任务可能挂在任意家庭成员上)
- */
- public List<Task> getTodayTasksByFamily(Long familyId) {
- if (familyId == null) {
- return new ArrayList<>();
- }
- Calendar calendar = Calendar.getInstance();
- calendar.set(Calendar.HOUR_OF_DAY, 0);
- calendar.set(Calendar.MINUTE, 0);
- calendar.set(Calendar.SECOND, 0);
- calendar.set(Calendar.MILLISECOND, 0);
- Date startOfDay = calendar.getTime();
- calendar.add(Calendar.DAY_OF_MONTH, 1);
- Date endOfDay = calendar.getTime();
- List<Task> all = taskMapper.selectList(new LambdaQueryWrapper<Task>()
- .eq(Task::getFamilyId, familyId)
- .ge(Task::getDeadline, startOfDay)
- .lt(Task::getDeadline, endOfDay)
- .in(Task::getStatus, Arrays.asList("pending", "completed", "locked", "pending_review"))
- .orderByAsc(Task::getDeadline)
- .orderByAsc(Task::getId));
- return filterLockedChildren(all);
- }
- /**
- * 移除"父任务未完成"的子任务(购买任务→每日使用子任务链)。
- * 子任务在父购买任务完成(status=completed)前保持锁定,不显示在今日任务中。
- */
- private List<Task> filterLockedChildren(List<Task> tasks) {
- if (tasks == null || tasks.isEmpty()) return tasks;
- java.util.Set<Long> parentIds = new java.util.HashSet<>();
- for (Task t : tasks) {
- if (t.getParentTaskId() != null) parentIds.add(t.getParentTaskId());
- }
- if (parentIds.isEmpty()) return tasks;
- Map<Long, Task> parents = new HashMap<>();
- if (!parentIds.isEmpty()) {
- List<Task> plist = taskMapper.selectBatchIds(parentIds);
- for (Task p : plist) parents.put(p.getId(), p);
- }
- List<Task> result = new java.util.ArrayList<>();
- for (Task t : tasks) {
- if (t.getParentTaskId() != null) {
- Task parent = parents.get(t.getParentTaskId());
- if (parent == null || !"completed".equals(parent.getStatus())) {
- continue; // 父购买任务未完成,子任务锁定不显示
- }
- }
- result.add(t);
- }
- return result;
- }
- /**
- * 按维度分类获取今日任务(五维页面筛选用)
- */
- public List<Task> getTodayTasksByCategory(Long memberId, String category) {
- Calendar calendar = Calendar.getInstance();
- calendar.set(Calendar.HOUR_OF_DAY, 0);
- calendar.set(Calendar.MINUTE, 0);
- calendar.set(Calendar.SECOND, 0);
- calendar.set(Calendar.MILLISECOND, 0);
- Date startOfDay = calendar.getTime();
- calendar.add(Calendar.DAY_OF_MONTH, 1);
- Date endOfDay = calendar.getTime();
- return filterLockedChildren(taskMapper.selectList(new LambdaQueryWrapper<Task>()
- .eq(Task::getFamilyMemberId, memberId)
- .eq(Task::getCategory, category)
- .ge(Task::getDeadline, startOfDay)
- .lt(Task::getDeadline, endOfDay)
- .in(Task::getStatus, Arrays.asList("pending", "completed", "pending_review"))
- .orderByAsc(Task::getDeadline)));
- }
- /**
- * 获取孩子的任务统计数据
- */
- public Map<String, Object> getChildTaskStats(Long memberId) {
- Map<String, Object> stats = new HashMap<>();
- Date now = new Date();
- Calendar cal = Calendar.getInstance();
- cal.setTime(now);
- cal.set(Calendar.HOUR_OF_DAY, 0);
- cal.set(Calendar.MINUTE, 0);
- cal.set(Calendar.SECOND, 0);
- cal.set(Calendar.MILLISECOND, 0);
- Date todayStart = cal.getTime();
- cal.set(Calendar.HOUR_OF_DAY, 23);
- cal.set(Calendar.MINUTE, 59);
- cal.set(Calendar.SECOND, 59);
- cal.set(Calendar.MILLISECOND, 999);
- Date todayEnd = cal.getTime();
- List<Task> allTasks = taskMapper.selectList(
- new LambdaQueryWrapper<Task>()
- .eq(Task::getFamilyMemberId, memberId)
- .between(Task::getDeadline, todayStart, todayEnd)
- );
- long completed = allTasks.stream()
- .filter(t -> "completed".equals(t.getStatus()))
- .count();
- int completionRate = allTasks.isEmpty() ? 0 : (int) (completed * 100 / allTasks.size());
- List<Task> pendingReview = taskMapper.selectList(
- new LambdaQueryWrapper<Task>()
- .eq(Task::getFamilyMemberId, memberId)
- .eq(Task::getNeedReview, 1)
- .eq(Task::getStatus, "pending")
- );
- long habitCount = allTasks.stream()
- .filter(t -> "completed".equals(t.getStatus()))
- .filter(t -> "habit".equals(t.getCategory()))
- .count();
- stats.put("completionRate", completionRate);
- stats.put("pendingReviewCount", pendingReview.size());
- stats.put("habitCount", (int) habitCount);
- return stats;
- }
- /**
- * TASK-SYS-248: 开始任务(两阶段任务入口)
- * 校验前置/会员/频率后:pending→in_progress,创建 task_executions(started)
- * 返回 executionId + 任务信息(前端据此跳转联动页面)
- */
- @Transactional
- public Map<String, Object> startTask(Long taskId, Long memberId) {
- Task task = taskMapper.selectById(taskId);
- if (task == null || !Objects.equals(task.getFamilyMemberId(), memberId)) {
- throw new IllegalArgumentException("任务不存在或不属于该成员");
- }
- if ("completed".equals(task.getStatus())) {
- throw new IllegalArgumentException("任务已完成");
- }
- // 前置任务校验
- if (task.getPrerequisiteTaskId() != null) {
- Task prereq = taskMapper.selectById(task.getPrerequisiteTaskId());
- if (prereq == null || !"completed".equals(prereq.getStatus())) {
- throw new IllegalArgumentException("需先完成前置任务:" + (prereq != null ? prereq.getTitle() : "未知任务"));
- }
- }
- Date now = new Date();
- // 状态流转:locked→pending 也允许开始(前端已解锁);其它状态置 in_progress
- task.setStatus("in_progress");
- task.setStartCount((task.getStartCount() == null ? 0 : task.getStartCount()) + 1);
- task.setUpdatedAt(now);
- taskMapper.updateById(task);
- // 创建执行记录
- TaskExecution exec = new TaskExecution();
- exec.setTaskId(taskId);
- exec.setMemberId(memberId);
- exec.setStatus("started");
- exec.setStartedAt(now);
- exec.setCreatedAt(now);
- taskExecutionMapper.insert(exec);
- // TASK-SYS-248: 购物任务联动——cart/order 动作自动加购/下单
- try {
- handleShoppingAction(task, memberId, exec, now);
- } catch (Exception e) {
- log.warn("购物任务联动失败(不影响任务开始): taskId={}, error={}", taskId, e.getMessage());
- }
- Map<String, Object> result = new HashMap<>();
- result.put("executionId", exec.getId());
- result.put("taskId", taskId);
- result.put("actionType", task.getActionType());
- result.put("actionConfig", task.getActionConfig());
- return result;
- }
- @Transactional
- public Map<String, Object> completeTask(Long taskId, Long memberId, String photoUrl, String content, String contentType) {
- Task task = taskMapper.selectById(taskId);
- if (task == null || !Objects.equals(task.getFamilyMemberId(), memberId)) {
- return null;
- }
- if ("completed".equals(task.getStatus()) || "pending_review".equals(task.getStatus())) {
- return null; // 任务已完成或已提交待审核,不能重复提交
- }
- // TASK-SYS-248: 前置任务校验——前置未完成则拒绝
- if (task.getPrerequisiteTaskId() != null) {
- Task prereq = taskMapper.selectById(task.getPrerequisiteTaskId());
- if (prereq == null || !"completed".equals(prereq.getStatus())) {
- throw new IllegalArgumentException("需先完成前置任务:" + (prereq != null ? prereq.getTitle() : "未知任务"));
- }
- }
- // TASK-SYS-248: 填写式完成强校验——require_input=1 必须带内容
- if (task.getRequireInput() != null && task.getRequireInput() == 1) {
- boolean hasContent = (content != null && !content.trim().isEmpty()) || (photoUrl != null && !photoUrl.isEmpty());
- if (!hasContent) {
- throw new IllegalArgumentException("该任务需填写内容后才能完成");
- }
- }
- // TASK-SYS-248: 两阶段任务校验——start_required=1 必须已开始(存在 in_progress 的 execution)
- if (task.getStartRequired() != null && task.getStartRequired() == 1) {
- Long executionCount = taskExecutionMapper.selectCount(
- new LambdaQueryWrapper<TaskExecution>()
- .eq(TaskExecution::getTaskId, taskId)
- .eq(TaskExecution::getMemberId, memberId)
- .eq(TaskExecution::getStatus, "started"));
- if (executionCount == null || executionCount == 0) {
- throw new IllegalArgumentException("该任务需先开始才能完成");
- }
- // 最短持续时长校验
- if (task.getMinDurationSeconds() != null && task.getMinDurationSeconds() > 0) {
- TaskExecution exec = taskExecutionMapper.selectOne(
- new LambdaQueryWrapper<TaskExecution>()
- .eq(TaskExecution::getTaskId, taskId)
- .eq(TaskExecution::getMemberId, memberId)
- .eq(TaskExecution::getStatus, "started")
- .last("LIMIT 1"));
- if (exec != null && exec.getStartedAt() != null) {
- long elapsed = (System.currentTimeMillis() - exec.getStartedAt().getTime()) / 1000;
- if (elapsed < task.getMinDurationSeconds()) {
- throw new IllegalArgumentException("该任务需持续至少 " + task.getMinDurationSeconds() + " 秒后才能完成");
- }
- }
- }
- }
- Date now = new Date();
- // 检查是否为小游戏任务
- if ("小游戏类".equals(task.getCategory()) && task.getMinigameCode() != null) {
- return null;
- }
- // 需审核的任务:提交后进入待审核,审核通过后再发放积分
- if (task.getNeedReview() != null && task.getNeedReview() == 1) {
- return submitTaskForReview(task, memberId, now, content, contentType);
- }
- recordTaskExecution(task, memberId, now, content, contentType);
- int pointsEarned = awardTaskCompletionRewards(task, memberId, now);
- FamilyMember child = familyMemberMapper.selectById(memberId);
- Map<String, Object> result = new HashMap<>();
- result.put("pointsEarned", pointsEarned);
- result.put("newBalance", child != null ? child.getTotalPoints() : 0);
- result.put("needReview", false);
- try {
- com.etotem.cfc.entity.TaskReminderConfig cfg = taskReminderConfigService.loadAllConfigs().get(task.getExecutorId());
- if (cfg != null && Integer.valueOf(1).equals(cfg.getCompletionPraiseEnabled())) {
- java.util.HashMap<String, Object> vars = new java.util.HashMap<String, Object>();
- vars.put("title", task.getTitle());
- String chans = cfg.getCompletionPraiseChannels();
- if (chans == null || chans.isEmpty()) chans = "wechat,in_app";
- notificationDispatchService.sendByTemplate("task_completion_praise", task.getExecutorId(), vars, "task", task.getId(),
- "任务完成,太棒了!",
- "太棒了!任务「" + task.getTitle() + "」已完成,继续保持,加油~");
- }
- } catch (Exception e) {}
- return result;
- }
- @Transactional
- public boolean reviewTask(Long taskId, TaskReviewDTO dto) {
- Task task = taskMapper.selectById(taskId);
- if (task == null) {
- return false;
- }
- if (!"pending_review".equals(task.getStatus())) {
- return "completed".equals(task.getStatus());
- }
- boolean isApproved;
- if (dto.getApproved() != null) {
- isApproved = dto.getApproved();
- } else if ("pass".equals(dto.getResult())) {
- isApproved = true;
- } else {
- isApproved = false;
- }
- if (isApproved) {
- Date completionTime = task.getCompletedAt() != null ? task.getCompletedAt() : new Date();
- awardTaskCompletionRewards(task, task.getFamilyMemberId(), completionTime);
- } else {
- task.setStatus("pending");
- task.setCompletedAt(null);
- task.setUpdatedAt(new Date());
- taskMapper.updateById(task);
- }
- return true;
- }
- public Page<Task> getTaskHistory(Long memberId, Integer page, Integer size) {
- Page<Task> pageParam = new Page<>(page, size);
- return taskMapper.selectPage(pageParam, new LambdaQueryWrapper<Task>()
- .eq(Task::getFamilyMemberId, memberId)
- .orderByDesc(Task::getCreatedAt));
- }
- public Page<Task> getTaskHistoryByCategory(Long memberId, Integer page, Integer size, String category) {
- Page<Task> pageParam = new Page<>(page, size);
- return taskMapper.selectPage(pageParam, new LambdaQueryWrapper<Task>()
- .eq(Task::getFamilyMemberId, memberId)
- .eq(Task::getCategory, category)
- .orderByDesc(Task::getCreatedAt));
- }
- public boolean deleteTask(Long taskId, Long userId) {
- Task task = taskMapper.selectById(taskId);
- if (task == null || !task.getCreatorId().equals(userId)) {
- return false;
- }
- task.setStatus("cancelled");
- task.setUpdatedAt(new Date());
- taskMapper.updateById(task);
- return true;
- }
- /**
- * 获取待审核任务列表
- * 家长查看需要审核的任务
- */
- public List<Task> getPendingReviewTasks(Long userId) {
- // 获取用户信息
- User user = userService.getUserInfo(userId);
- if (user == null || !"parent".equals(user.getRole())) {
- return new ArrayList<>();
- }
- // 查询该家庭下所有需要审核且已完成的任务
- LambdaQueryWrapper<Task> wrapper = new LambdaQueryWrapper<>();
- wrapper.eq(Task::getFamilyId, user.getFamilyId())
- .eq(Task::getNeedReview, 1)
- .eq(Task::getStatus, "pending_review")
- .orderByDesc(Task::getCompletedAt);
- SortUtil.applySort(wrapper);
- return taskMapper.selectList(wrapper);
- }
- /**
- * 获取家长今日任务 (家长作为执行者的任务)
- */
- public List<Task> getTodayParentTasks(Long userId) {
- Calendar calendar = Calendar.getInstance();
- calendar.set(Calendar.HOUR_OF_DAY, 0);
- calendar.set(Calendar.MINUTE, 0);
- calendar.set(Calendar.SECOND, 0);
- calendar.set(Calendar.MILLISECOND, 0);
- Date startOfDay = calendar.getTime();
- calendar.add(Calendar.DAY_OF_MONTH, 1);
- Date endOfDay = calendar.getTime();
- User user = userService.getUserInfo(userId);
- if (user == null) {
- return new ArrayList<>();
- }
- return taskMapper.selectList(new LambdaQueryWrapper<Task>()
- .eq(Task::getFamilyId, user.getFamilyId())
- .eq(Task::getExecutorType, "parent")
- .eq(Task::getExecutorId, userId)
- .ge(Task::getDeadline, startOfDay)
- .lt(Task::getDeadline, endOfDay)
- .in(Task::getStatus, Arrays.asList("pending", "completed"))
- .orderByAsc(Task::getDeadline));
- }
- /**
- * 家长完成任务 (家长作为执行者完成任务)
- */
- @Transactional
- public Map<String, Object> completeParentTask(Long taskId, Long userId) {
- Task task = taskMapper.selectById(taskId);
- if (task == null || !("parent".equals(task.getExecutorType()) && task.getExecutorId().equals(userId))) {
- return null;
- }
- if ("completed".equals(task.getStatus())) {
- return null;
- }
- User user = userMapper.selectById(userId);
- if (user == null) {
- return null;
- }
- Date now = new Date();
- int pointsEarned = task.getPoints();
- boolean isEarly = now.before(task.getDeadline());
- // 提前完成获得奖励分
- if (isEarly) {
- pointsEarned += EARLY_BONUS;
- }
- // 更新任务状态
- task.setStatus("completed");
- task.setCompletedAt(now);
- task.setUpdatedAt(new Date());
- taskMapper.updateById(task);
- // 仅编排节点任务生效:通知编排引擎流程评估(非编排节点任务零开销)
- try {
- orchestrationEngine.onTaskCompleted(taskId);
- } catch (Exception e) {
- log.warn("orchestration onTaskCompleted failed: {}", e.getMessage());
- }
- // 更新家长积分
- int newPoints = (user.getTotalPoints() != null ? user.getTotalPoints() : 0) + pointsEarned;
- user.setTotalPoints(newPoints);
- user.setUpdatedAt(new Date());
- userMapper.updateById(user);
- // 记录积分流水
- PointsLog log = new PointsLog();
- log.setChildId(task.getFamilyMemberId());
- log.setAmount(pointsEarned);
- log.setType(isEarly ? "earn" : "earn");
- log.setDescription("完成任务: " + task.getTitle());
- log.setCreatedAt(new Date());
- pointsLogMapper.insert(log);
- Map<String, Object> result = new HashMap<>();
- result.put("pointsEarned", pointsEarned);
- try { FamilyMember child = familyMemberMapper.selectById(task.getFamilyMemberId()); if (child != null) { this.completeOnboardingTask(child.getUserId(), "FIRST_TASK"); } } catch (Exception e) { /* onboarding trigger failure should not block task completion */ }
- result.put("newBalance", user.getTotalPoints());
- result.put("needReview", task.getNeedReview() == 1);
- try {
- Long receiver = userId;
- com.etotem.cfc.entity.TaskReminderConfig cfg = taskReminderConfigService.loadAllConfigs().get(receiver);
- if (cfg != null && Integer.valueOf(1).equals(cfg.getCompletionPraiseEnabled())) {
- java.util.HashMap<String, Object> vars = new java.util.HashMap<String, Object>();
- vars.put("title", task.getTitle());
- notificationDispatchService.sendByTemplate("task_completion_praise", receiver, vars, "task", task.getId(),
- "任务完成,太棒了!",
- "太棒了!任务「" + task.getTitle() + "」已完成,继续保持,加油~");
- }
- } catch (Exception e) {}
- return result;
- }
- public void batchComplete(List<Long> taskIds, Long userId) {
- taskMapper.update(null, new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper<Task>()
- .in(Task::getId, taskIds)
- .and(w -> w.eq(Task::getCreatorId, userId).or().eq(Task::getExecutorId, userId))
- .set(Task::getStatus, "completed")
- .set(Task::getCompletedAt, new Date()));
- }
- public void batchDelete(List<Long> taskIds, Long userId) {
- taskMapper.update(null, new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper<Task>()
- .in(Task::getId, taskIds)
- .eq(Task::getCreatorId, userId)
- .set(Task::getStatus, "cancelled")
- .set(Task::getUpdatedAt, new Date()));
- }
- private Map<String, Object> submitTaskForReview(Task task, Long memberId, Date now, String content, String contentType) {
- task.setStatus("pending_review");
- task.setCompletedAt(now);
- task.setUpdatedAt(now);
- taskMapper.updateById(task);
- recordTaskExecution(task, memberId, now, content, contentType);
- FamilyMember child = familyMemberMapper.selectById(memberId);
- Map<String, Object> result = new HashMap<>();
- result.put("pointsEarned", 0);
- result.put("newBalance", child != null ? child.getTotalPoints() : 0);
- result.put("needReview", true);
- result.put("message", "已提交,待审核");
- return result;
- }
- private int awardTaskCompletionRewards(Task task, Long memberId, Date completionTime) {
- Long taskId = task.getId();
- int pointsEarned = task.getPoints() != null ? task.getPoints() : 0;
- boolean isEarly = task.getDeadline() != null && completionTime.before(task.getDeadline());
- boolean isLateGrace = false;
- if (!isEarly && task.getDeadline() != null) {
- long diffMinutes = (completionTime.getTime() - task.getDeadline().getTime()) / (1000 * 60);
- isLateGrace = diffMinutes <= LATE_GRACE_MINUTES;
- }
- FamilyMember child = familyMemberMapper.selectById(memberId);
- if (child == null) {
- return 0;
- }
- String bonusRuleJson = task.getBonusRule();
- if (bonusRuleJson != null && !bonusRuleJson.isEmpty()) {
- pointsEarned = calculatePointsWithBonusRule(task, completionTime, isEarly, isLateGrace, child);
- } else if (isEarly || isLateGrace) {
- pointsEarned += EARLY_BONUS;
- } else {
- pointsEarned = calculateLatePenalty(task, child, memberId);
- }
- task.setStatus("completed");
- task.setCompletedAt(completionTime);
- task.setUpdatedAt(new Date());
- taskMapper.updateById(task);
- child.setTotalPoints(child.getTotalPoints() + pointsEarned);
- Date lastTaskDate = child.getLastTaskDate();
- Date today = new Date();
- Calendar cal = Calendar.getInstance();
- cal.add(Calendar.DAY_OF_MONTH, -1);
- Date yesterday = cal.getTime();
- if (lastTaskDate == null || lastTaskDate.before(yesterday)) {
- child.setStreakDays(1);
- } else if (!isSameDay(lastTaskDate, today)) {
- child.setStreakDays(child.getStreakDays() + 1);
- }
- child.setLastTaskDate(today);
- child.setUpdatedAt(new Date());
- familyMemberMapper.updateById(child);
- int milestoneReward = streakService.checkAndAwardMilestone(memberId, child.getStreakDays());
- if (milestoneReward > 0) {
- pointsEarned += milestoneReward;
- log.info("孩子{}获得打卡里程碑奖励{}积分", memberId, milestoneReward);
- }
- PointsLog pointsLog = new PointsLog();
- pointsLog.setChildId(memberId);
- pointsLog.setTaskId(taskId);
- pointsLog.setAmount(pointsEarned);
- if (pointsEarned > 0) {
- pointsLog.setType(isEarly ? "bonus" : "earn");
- } else if (pointsEarned < 0) {
- pointsLog.setType("penalty");
- } else {
- pointsLog.setType("earn");
- }
- pointsLog.setDescription("完成任务: " + task.getTitle());
- pointsLog.setCreatedAt(new Date());
- if (pointsEarned > 0 && task.getPointsExpireDays() != null && task.getPointsExpireDays() > 0) {
- Calendar expireCal = Calendar.getInstance();
- expireCal.add(Calendar.DAY_OF_MONTH, task.getPointsExpireDays());
- pointsLog.setExpireAt(expireCal.getTime());
- }
- pointsLogMapper.insert(pointsLog);
- try {
- int energyAmount = Math.abs(pointsEarned);
- if (energyAmount > 0) {
- energyService.awardEnergy(memberId, "task", taskId, energyAmount,
- "完成任务: " + task.getTitle(), null);
- }
- } catch (Exception e) {
- log.warn("能量发放失败(不影响积分): memberId={}, taskId={}, error={}",
- memberId, taskId, e.getMessage());
- }
- try {
- if (child.getUserId() != null) {
- this.updateProgress(child.getUserId(), child.getId(), "DAILY_TASK", 1);
- }
- } catch (Exception e) {
- log.warn("成长任务进度更新失败: memberId={}, error={}", memberId, e.getMessage());
- }
- // D5: 打卡完成发放 CF 值(用户级 + 家庭级)。
- // earn() 按 (id, ref_type, ref_id) 幂等去重,审核重复触发不会双记。
- // CF 额度取任务基础积分,不含连胜里程碑加成与迟到惩罚,避免奖励被放大/倒挂。
- try {
- int cfAmount = task.getPoints() != null && task.getPoints() > 0 ? task.getPoints() : 0;
- // 注册/绑定推荐人为引流类任务,不发可提现的CF值(积分照发,防止刷CF)
- if (cfAmount > 0 && ("REGISTER".equals(task.getTitle()) || "BIND_REFERRAL".equals(task.getTitle()))) {
- cfAmount = 0;
- }
- if (cfAmount > 0) {
- String remark = "完成任务: " + task.getTitle();
- if (child != null && child.getUserId() != null) {
- platformPointsService.earn(child.getUserId(), cfAmount, "task_complete", taskId, remark);
- }
- if (task.getFamilyId() != null) {
- familyPlatformPointsService.earn(task.getFamilyId(), cfAmount, "task_complete", taskId, remark);
- }
- }
- } catch (Exception e) {
- log.warn("CF值发放失败(不影响积分): taskId={}, error={}", taskId, e.getMessage());
- }
- unlockPrerequisiteTasks(taskId);
- return pointsEarned;
- }
- private void recordTaskExecution(Task task, Long memberId, Date now, String content, String contentType) {
- Long taskId = task.getId();
- try {
- TaskExecution activeExec = null;
- if (task.getStartRequired() != null && task.getStartRequired() == 1) {
- activeExec = taskExecutionMapper.selectOne(
- new LambdaQueryWrapper<TaskExecution>()
- .eq(TaskExecution::getTaskId, taskId)
- .eq(TaskExecution::getMemberId, memberId)
- .eq(TaskExecution::getStatus, "started")
- .last("LIMIT 1"));
- }
- if (activeExec != null) {
- activeExec.setStatus("finished");
- activeExec.setFinishedAt(now);
- activeExec.setContent(content);
- activeExec.setContentType(contentType);
- taskExecutionMapper.updateById(activeExec);
- } else if (content != null && !content.trim().isEmpty()) {
- TaskExecution exec = new TaskExecution();
- exec.setTaskId(taskId);
- exec.setMemberId(memberId);
- exec.setStatus("finished");
- exec.setStartedAt(now);
- exec.setFinishedAt(now);
- exec.setContent(content);
- exec.setContentType(contentType);
- exec.setCreatedAt(now);
- taskExecutionMapper.insert(exec);
- }
- } catch (Exception e) {
- log.warn("任务执行进度记录失败(不影响完成): taskId={}, error={}", taskId, e.getMessage());
- }
- }
- private void unlockPrerequisiteTasks(Long taskId) {
- try {
- List<Task> nextTasks = taskMapper.selectList(
- new LambdaQueryWrapper<Task>()
- .eq(Task::getPrerequisiteTaskId, taskId));
- for (Task next : nextTasks) {
- if ("locked".equals(next.getStatus())) {
- next.setStatus("pending");
- next.setUpdatedAt(new Date());
- taskMapper.updateById(next);
- log.info("前置任务{}完成,解锁后置任务{}", taskId, next.getId());
- }
- }
- } catch (Exception e) {
- log.warn("后置任务解锁失败: taskId={}, error={}", taskId, e.getMessage());
- }
- }
- /**
- * 管理员/平台查询待审核任务
- */
- public List<Task> getAllPendingReviewTasks(Long familyId) {
- LambdaQueryWrapper<Task> wrapper = new LambdaQueryWrapper<Task>()
- .eq(Task::getNeedReview, 1)
- .eq(Task::getStatus, "pending_review")
- .orderByDesc(Task::getCompletedAt);
- if (familyId != null) {
- wrapper.eq(Task::getFamilyId, familyId);
- }
- SortUtil.applySort(wrapper);
- return taskMapper.selectList(wrapper);
- }
- private boolean isSameDay(Date date1, Date date2) {
- Calendar cal1 = Calendar.getInstance();
- cal1.setTime(date1);
- Calendar cal2 = Calendar.getInstance();
- cal2.setTime(date2);
- return cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR)
- && cal1.get(Calendar.DAY_OF_YEAR) == cal2.get(Calendar.DAY_OF_YEAR);
- }
- /**
- * 获取孩子的待审核任务
- */
- public List<Task> getPendingReviewTasksForChild(Long memberId) {
- return taskMapper.selectList(new LambdaQueryWrapper<Task>()
- .eq(Task::getFamilyMemberId, memberId)
- .eq(Task::getNeedReview, 1)
- .eq(Task::getStatus, "pending_review")
- .orderByDesc(Task::getCompletedAt));
- }
- /**
- * 获取孩子的所有任务
- */
- public List<Task> getAllTasksByChild(Long memberId) {
- return taskMapper.selectList(new LambdaQueryWrapper<Task>()
- .eq(Task::getFamilyMemberId, memberId)
- .orderByDesc(Task::getCreatedAt));
- }
- /**
- * 获取孩子的任务历史(分页)
- */
- public Page<Task> getTaskHistoryByChild(Long memberId, Integer page, Integer size, String status) {
- Page<Task> pageParam = new Page<>(page, size);
- LambdaQueryWrapper<Task> wrapper = new LambdaQueryWrapper<Task>()
- .eq(Task::getFamilyMemberId, memberId)
- .orderByDesc(Task::getCreatedAt);
-
- if (status != null && !status.isEmpty()) {
- wrapper.eq(Task::getStatus, status);
- }
- SortUtil.applySort(wrapper);
- return taskMapper.selectPage(pageParam, wrapper);
- }
- /**
- * POINT-002: 根据自定义规则计算超额积分
- * bonusRule JSON格式: {
- * "earlyBonus": 1, // 提前完成额外积分
- * "earlyMinutes": 30, // 提前多少分钟才算提前(可选)
- * "qualityBonus": 2, // 超质完成额外积分(家长评分)
- * "qualityThreshold": 90 // 质量评分阈值(可选)
- * }
- */
- private int calculatePointsWithBonusRule(Task task, Date completedAt, boolean isEarly, boolean isLateGrace, FamilyMember child) {
- int points = task.getPoints();
- try {
- // 解析JSON规则
- String json = task.getBonusRule();
- if (json == null || json.isEmpty()) {
- return isEarly || isLateGrace ? points + EARLY_BONUS : calculateLatePenalty(task, child, child.getId());
- }
- // 简单JSON解析(避免引入额外依赖)
- Integer earlyBonus = extractJsonValue(json, "earlyBonus");
- Integer earlyMinutes = extractJsonValue(json, "earlyMinutes");
- Integer qualityBonus = extractJsonValue(json, "qualityBonus");
- // 提前完成奖励
- if (isEarly || isLateGrace) {
- if (earlyBonus != null) {
- points += earlyBonus;
- } else {
- points += EARLY_BONUS; // 默认+1
- }
- // 如果设置了提前分钟数,检查是否满足条件
- if (earlyMinutes != null && task.getDeadline() != null) {
- long diffMinutes = (task.getDeadline().getTime() - completedAt.getTime()) / (1000 * 60);
- if (diffMinutes < earlyMinutes) {
- // 不满足提前分钟数,只给基础分
- points = task.getPoints();
- }
- }
- } else {
- // 超时,计算扣分
- points = calculateLatePenalty(task, child, child.getId());
- }
- // 超质完成奖励(需要家长后续评分,这里先不处理)
- // qualityBonus 会在家长审核时根据评分添加
- } catch (Exception e) {
- log.warn("解析超额积分规则失败: {}", e.getMessage());
- // 解析失败,使用默认规则
- if (isEarly || isLateGrace) {
- points += EARLY_BONUS;
- } else {
- points = calculateLatePenalty(task, child, child.getId());
- }
- }
- return points;
- }
- /**
- * 从简单JSON字符串中提取整数值
- */
- private Integer extractJsonValue(String json, String key) {
- try {
- String pattern = "\"" + key + "\"\\s*:\\s*(\\d+)";
- java.util.regex.Pattern p = java.util.regex.Pattern.compile(pattern);
- java.util.regex.Matcher m = p.matcher(json);
- if (m.find()) {
- return Integer.parseInt(m.group(1));
- }
- } catch (Exception e) {
- log.warn("提取JSON值失败: key={}", key);
- }
- return null;
- }
- /**
- * 计算超时扣分
- */
- private int calculateLatePenalty(Task task, FamilyMember child, Long memberId) {
- if (child == null) {
- return 0;
- }
- int points = task.getPoints() != null ? task.getPoints() : 0;
- // 无 deadline 的每日进度型任务(饮食/睡眠/运动打卡等)不存在"迟到"概念,发放基础分
- if (task.getDeadline() == null) {
- return points;
- }
- if (child.getPenaltyEnabled() != null && child.getPenaltyEnabled() == 1) {
- // 检查今日已扣分数
- Calendar cal = Calendar.getInstance();
- cal.set(Calendar.HOUR_OF_DAY, 0);
- cal.set(Calendar.MINUTE, 0);
- cal.set(Calendar.SECOND, 0);
- Date startOfDay = cal.getTime();
- Integer todayPenalty = pointsLogMapper.selectSum(
- new LambdaQueryWrapper<PointsLog>()
- .eq(PointsLog::getChildId, memberId)
- .eq(PointsLog::getType, "penalty")
- .ge(PointsLog::getCreatedAt, startOfDay)
- );
- int todayPenaltyTotal = todayPenalty != null ? Math.abs(todayPenalty) : 0;
- if (todayPenaltyTotal < PENALTY_MAX_DAILY) {
- int penalty = Math.min(task.getPoints(), PENALTY_MAX_DAILY - todayPenaltyTotal);
- return -penalty;
- } else {
- return points; // 达到每日扣分上限:不扣分,任务完成仍发放基础分
- }
- } else {
- return points; // 扣分未开启:任务完成发放基础分
- }
- }
- /**
- * 完成小游戏任务
- * @param taskId 任务ID
- * @param memberId 孩子ID
- * @param completionTime 完成用时(秒)
- * @param score 得分(可选)
- * @return 积分信息
- */
- @Transactional
- public Map<String, Object> completeMinigameTask(Long taskId, Long memberId, Integer completionTime, Integer score) {
- Task task = taskMapper.selectById(taskId);
- if (task == null || !Objects.equals(task.getFamilyMemberId(), memberId)) {
- throw new RuntimeException("任务不存在或不属于该孩子");
- }
- if ("completed".equals(task.getStatus())) {
- throw new RuntimeException("任务已完成");
- }
- if (!"小游戏类".equals(task.getCategory()) || task.getMinigameCode() == null) {
- throw new RuntimeException("该任务不是小游戏任务");
- }
- // 调用MiniGameService计算积分
- Map<String, Object> gameResult = miniGameService.completeGame(memberId, task.getMinigameCode(), completionTime, score);
- int pointsEarned = (Integer) gameResult.get("pointsEarned");
- // 更新任务状态
- task.setStatus("completed");
- task.setCompletedAt(new Date());
- task.setUpdatedAt(new Date());
- taskMapper.updateById(task);
- // 更新连续打卡
- FamilyMember child = familyMemberMapper.selectById(memberId);
- Date lastTaskDate = child.getLastTaskDate();
- Date today = new Date();
- Calendar cal = Calendar.getInstance();
- cal.add(Calendar.DAY_OF_MONTH, -1);
- Date yesterday = cal.getTime();
- if (lastTaskDate == null || lastTaskDate.before(yesterday)) {
- child.setStreakDays(1); // 重新开始
- } else if (lastTaskDate != null && isSameDay(lastTaskDate, today)) {
- // 今天已经完成过任务,不增加天数
- } else {
- child.setStreakDays(child.getStreakDays() + 1);
- }
- child.setLastTaskDate(today);
- child.setUpdatedAt(new Date());
- familyMemberMapper.updateById(child);
- // 检查并发放打卡里程碑奖励
- int milestoneReward = streakService.checkAndAwardMilestone(memberId, child.getStreakDays());
- if (milestoneReward > 0) {
- pointsEarned += milestoneReward;
- log.info("孩子{}获得打卡里程碑奖励{}积分", memberId, milestoneReward);
- }
- Map<String, Object> result = new HashMap<>();
- result.put("pointsEarned", pointsEarned);
- result.put("newBalance", child.getTotalPoints());
- result.put("gameName", gameResult.get("gameName"));
- result.put("needReview", task.getNeedReview() == 1);
- return result;
- }
- /**
- * 获取家庭待办任务
- */
- public List<Task> getPendingTasksByFamily(Long familyId) {
- return taskMapper.selectList(new LambdaQueryWrapper<Task>()
- .eq(Task::getFamilyId, familyId)
- .in(Task::getStatus, Arrays.asList("pending", "in_progress"))
- .orderByAsc(Task::getDeadline));
- }
- // ==================== Family Member Task queries ====================
- /**
- * 获取指定家庭成员待审核的任务列表
- */
- public List<Task> getPendingReviewTasksForFamilyMember(Long familyMemberId) {
- return taskMapper.selectList(new LambdaQueryWrapper<Task>()
- .eq(Task::getFamilyMemberId, familyMemberId)
- .eq(Task::getNeedReview, 1)
- .eq(Task::getStatus, "pending_review")
- .orderByDesc(Task::getCompletedAt));
- }
- /**
- * 获取指定家庭成员的全部任务
- */
- public List<Task> getAllTasksByFamilyMember(Long familyMemberId) {
- return taskMapper.selectList(new LambdaQueryWrapper<Task>()
- .eq(Task::getFamilyMemberId, familyMemberId)
- .orderByDesc(Task::getCreatedAt));
- }
- /**
- * 分页获取指定家庭成员的任务历史
- */
- public Page<Task> getTaskHistoryByFamilyMember(Long familyMemberId, Integer page, Integer size, String status) {
- LambdaQueryWrapper<Task> wrapper = new LambdaQueryWrapper<Task>()
- .eq(Task::getFamilyMemberId, familyMemberId);
- if (status != null && !status.isEmpty()) {
- wrapper.eq(Task::getStatus, status);
- }
- wrapper.orderByDesc(Task::getCreatedAt);
- SortUtil.applySort(wrapper);
- return taskMapper.selectPage(new Page<>(page, size), wrapper);
- }
- /**
- * 获取当前用户收到的待接受任务列表(executor_type='member' AND status='assigned')
- */
- public List<Task> getMyPendingTasks(Long memberId) {
- Calendar calendar = Calendar.getInstance();
- calendar.set(Calendar.HOUR_OF_DAY, 0);
- calendar.set(Calendar.MINUTE, 0);
- calendar.set(Calendar.SECOND, 0);
- calendar.set(Calendar.MILLISECOND, 0);
- Date startOfDay = calendar.getTime();
- calendar.add(Calendar.DAY_OF_MONTH, 1);
- Date endOfDay = calendar.getTime();
- return taskMapper.selectList(new LambdaQueryWrapper<Task>()
- .eq(Task::getExecutorType, "member")
- .eq(Task::getExecutorId, memberId)
- .eq(Task::getStatus, "assigned")
- .ge(Task::getDeadline, startOfDay)
- .lt(Task::getDeadline, endOfDay)
- .or()
- .and(w -> w.eq(Task::getExecutorType, "member")
- .eq(Task::getExecutorId, memberId)
- .eq(Task::getStatus, "assigned")
- .isNull(Task::getDeadline))
- .orderByAsc(Task::getDeadline));
- }
- /**
- * 接受任务:status 从 assigned 改为 in_progress
- */
- @Transactional
- public boolean acceptTask(Long taskId, Long memberId) {
- Task task = taskMapper.selectById(taskId);
- if (task == null || !task.getExecutorId().equals(memberId) || !"assigned".equals(task.getStatus())) {
- return false;
- }
- task.setStatus("in_progress");
- task.setAcceptDeadline(null);
- task.setUpdatedAt(new Date());
- taskMapper.updateById(task);
- return true;
- }
- /**
- * 拒绝任务:status 改为 rejected,调整 creator→assignee 和 assignee→creator 的关系分数
- */
- @Transactional
- public boolean rejectTask(Long taskId, Long memberId) {
- Task task = taskMapper.selectById(taskId);
- if (task == null || !task.getExecutorId().equals(memberId) || !"assigned".equals(task.getStatus())) {
- return false;
- }
- task.setStatus("rejected");
- task.setUpdatedAt(new Date());
- taskMapper.updateById(task);
- Long creatorId = task.getCreatorId();
- Long assigneeId = memberId;
- Long familyId = task.getFamilyId();
- if (creatorId == null || assigneeId == null || familyId == null) return true;
- int trustDelta = -1;
- int intimacyDelta = -1;
- int commDelta = -1;
- adjustRelationshipScore(familyId, creatorId, assigneeId, trustDelta, intimacyDelta, commDelta);
- adjustRelationshipScore(familyId, assigneeId, creatorId, 0, 0, 1);
- return true;
- }
- private void adjustRelationshipScore(Long familyId, Long fromId, Long toId, int trustDelta, int intimacyDelta, int commDelta) {
- FamilyRelationshipScore score = familyRelationshipScoreMapper.selectOne(
- new LambdaQueryWrapper<FamilyRelationshipScore>()
- .eq(FamilyRelationshipScore::getFamilyId, familyId)
- .eq(FamilyRelationshipScore::getFromMemberId, fromId)
- .eq(FamilyRelationshipScore::getToMemberId, toId));
- if (score == null) {
- FamilyMember from = familyMemberMapper.selectById(fromId);
- score = new FamilyRelationshipScore();
- score.setFamilyId(familyId);
- score.setFromMemberId(fromId);
- score.setToMemberId(toId);
- score.setTrustScore(from != null && from.getTrustScore() != null ? from.getTrustScore() : new java.math.BigDecimal("60.00"));
- score.setIntimacyScore(from != null && from.getIntimacyScore() != null ? from.getIntimacyScore() : new java.math.BigDecimal("60.00"));
- score.setCommunicationScore(from != null && from.getCommunicationScore() != null ? from.getCommunicationScore() : new java.math.BigDecimal("60.00"));
- try {
- familyRelationshipScoreMapper.insert(score);
- } catch (Exception e) {
- score = familyRelationshipScoreMapper.selectOne(
- new LambdaQueryWrapper<FamilyRelationshipScore>()
- .eq(FamilyRelationshipScore::getFamilyId, familyId)
- .eq(FamilyRelationshipScore::getFromMemberId, fromId)
- .eq(FamilyRelationshipScore::getToMemberId, toId));
- }
- }
- if (score == null) return;
- if (trustDelta != 0) score.setTrustScore(score.getTrustScore().add(java.math.BigDecimal.valueOf(trustDelta)));
- if (intimacyDelta != 0) score.setIntimacyScore(score.getIntimacyScore().add(java.math.BigDecimal.valueOf(intimacyDelta)));
- if (commDelta != 0) score.setCommunicationScore(score.getCommunicationScore().add(java.math.BigDecimal.valueOf(commDelta)));
- familyRelationshipScoreMapper.updateById(score);
- }
- /**
- * TASK-SYS-248: 购物任务联动(cart/order 动作的自动加购/下单)
- * 在任务开始(startTask)时被调用,将 actionConfig 解析后驱动 CartService / ProductOrderService
- * actionConfig JSON 格式示例:
- * cart: {"productId": 123, "quantity": 1, "skuId": 456}
- * order: {"productId": 123, "quantity": 1, "skuId": 456, "consigneeId": 789, "deliveryMethod": 1}
- */
- private void handleShoppingAction(Task task, Long memberId, TaskExecution exec, Date now) {
- String actionType = task.getActionType();
- if (actionType == null || actionType.isEmpty()) {
- return;
- }
- if (!"cart".equals(actionType) && !"order".equals(actionType)) {
- return;
- }
- JSONObject config = parseActionConfig(task.getActionConfig());
- Long productId = config != null ? config.getLong("productId") : null;
- Integer quantity = config != null && config.getInteger("quantity") != null ? config.getInteger("quantity") : 1;
- Long skuId = config != null ? config.getLong("skuId") : null;
- if (productId == null) {
- log.warn("购物任务缺少 productId: taskId={}", task.getId());
- return;
- }
- // 从 member 获取 userId
- FamilyMember member = familyMemberMapper.selectById(memberId);
- Long userId = member != null ? member.getUserId() : null;
- if (userId == null) {
- log.warn("购物任务无法找到 userId: memberId={}", memberId);
- return;
- }
- JSONObject extra = new JSONObject();
- extra.put("actionType", actionType);
- extra.put("productId", productId);
- extra.put("quantity", quantity);
- if (skuId != null) {
- extra.put("skuId", skuId);
- }
- try {
- // 加购(cart 和 order 都需要)
- cartService.addToCart(userId, productId, quantity, skuId);
- extra.put("cartAdded", true);
- if ("order".equals(actionType)) {
- // 创建 pending 订单
- Long consigneeId = config != null ? config.getLong("consigneeId") : null;
- Integer deliveryMethod = config != null ? config.getInteger("deliveryMethod") : null;
- // 未指定收货地址时取用户默认地址
- if (consigneeId == null) {
- try {
- Consignee defaultConsignee = consigneeMapper.selectOne(
- new LambdaQueryWrapper<Consignee>()
- .eq(Consignee::getUserId, userId)
- .eq(Consignee::getIsDefault, 1)
- .last("LIMIT 1"));
- if (defaultConsignee != null) {
- consigneeId = defaultConsignee.getId();
- }
- } catch (Exception e) {
- log.warn("查询默认收货地址失败: userId={}", userId);
- }
- }
- CreateProductOrderDTO orderDto = new CreateProductOrderDTO();
- orderDto.setProductId(productId);
- orderDto.setQuantity(quantity);
- orderDto.setPaymentMethod("wechat");
- if (deliveryMethod != null) {
- orderDto.setDeliveryMethod(deliveryMethod);
- }
- if (consigneeId != null) {
- orderDto.setConsigneeId(consigneeId);
- }
- Result<ProductOrderDTO> orderResult = productOrderService.create(orderDto, userId);
- if (orderResult.getCode() == 200 && orderResult.getData() != null) {
- extra.put("orderId", orderResult.getData().getId());
- extra.put("orderNo", orderResult.getData().getOrderNo());
- exec.setExtra(JSON.toJSONString(extra));
- taskExecutionMapper.updateById(exec);
- log.info("购物任务下单成功: taskId={}, orderNo={}", task.getId(), orderResult.getData().getOrderNo());
- } else {
- // 下单失败(通常因缺地址/库存),保留加购状态
- extra.put("orderFailed", true);
- extra.put("orderError", orderResult.getMessage());
- exec.setExtra(JSON.toJSONString(extra));
- taskExecutionMapper.updateById(exec);
- log.warn("购物任务下单失败: taskId={}, reason={}", task.getId(), orderResult.getMessage());
- }
- } else {
- // 纯加购
- exec.setExtra(JSON.toJSONString(extra));
- taskExecutionMapper.updateById(exec);
- }
- } catch (Exception e) {
- log.error("购物任务联动失败: taskId={}, error={}", task.getId(), e.getMessage(), e);
- extra.put("error", e.getMessage());
- exec.setExtra(JSON.toJSONString(extra));
- taskExecutionMapper.updateById(exec);
- }
- }
- private com.alibaba.fastjson.JSONObject parseActionConfig(String json) {
- if (json == null || json.trim().isEmpty()) {
- return null;
- }
- try {
- return JSON.parseObject(json);
- } catch (Exception e) {
- log.warn("解析 actionConfig 失败: {}", json);
- return null;
- }
- }
- /**
- * TASK-SYS-248: 调研任务自动模板生成(actionType=survey 时,若 actionConfig 无 surveyId 则调 AI 生成)
- * 在 createTask 的 taskMapper.insert() 之后调用
- */
- private void ensureSurveyTemplateForTask(Task task) {
- if (!"survey".equals(task.getActionType())) {
- return;
- }
- JSONObject config = parseActionConfig(task.getActionConfig());
- // 已有 surveyId → 跳过
- if (config != null && config.get("surveyId") != null) {
- return;
- }
- // 从 config 或 task 属性取维度和主题
- String dimension = null;
- String topic = null;
- if (config != null) {
- dimension = config.getString("dimension");
- topic = config.getString("topic");
- }
- if (dimension == null || dimension.isEmpty()) {
- dimension = task.getDimensionCode();
- }
- if (topic == null || topic.isEmpty()) {
- topic = task.getTitle();
- }
- if (topic == null || topic.isEmpty()) {
- topic = "综合评估";
- }
- try {
- String questionsJson = surveyService.aiGenerateQuestions(dimension, topic);
- SurveyTemplate tpl = new SurveyTemplate();
- tpl.setTitle(task.getTitle() != null ? task.getTitle() + " - 调研" : "定期调研");
- tpl.setDimension(dimension);
- tpl.setQuestionsJson(questionsJson);
- tpl.setCycleType("weekly");
- tpl.setEnabled(1);
- tpl.setAiGenerated(1);
- tpl.setCreatedAt(new Date());
- tpl.setUpdatedAt(new Date());
- surveyTemplateMapper.insert(tpl);
- // 回填 actionConfig
- if (config == null) {
- config = new JSONObject();
- }
- config.put("surveyId", tpl.getId());
- task.setActionConfig(config.toJSONString());
- taskMapper.updateById(task);
- log.info("调研任务自动生成模板: taskId={}, templateId={}, dimension={}, topic={}",
- task.getId(), tpl.getId(), dimension, topic);
- } catch (Exception e) {
- log.warn("调研任务自动生成模板失败(不影响任务创建): taskId={}, error={}", task.getId(), e.getMessage());
- }
- }
- // ==================== 统一任务系统:成长任务迁移(原 GrowthTaskService) ====================
- /**
- * 回填进度型任务的当前进度(progress/todayDone,不入库)
- */
- private void fillProgress(List<Task> tasks, Long memberId) {
- if (tasks == null || tasks.isEmpty()) {
- return;
- }
- for (Task t : tasks) {
- if (t.getIsDailyProgress() != null && t.getIsDailyProgress() == 1) {
- TaskExecution exec = taskExecutionMapper.selectOne(new LambdaQueryWrapper<TaskExecution>()
- .eq(TaskExecution::getTaskId, t.getId())
- .eq(TaskExecution::getMemberId, memberId)
- .last("LIMIT 1"));
- int progress = 0;
- if (exec != null && exec.getExtra() != null) {
- try {
- progress = JSON.parseObject(exec.getExtra()).getIntValue("progress");
- } catch (Exception ignore) {
- }
- }
- t.setProgress(progress);
- t.setTodayDone("completed".equals(t.getStatus()) ? 1 : 0);
- }
- }
- }
- /**
- * 统一任务系统:更新每日进度型任务的进度(原 growth_task updateProgress)
- * 按 (userId→familyMemberId, taskKey) 找当日任务实例,累计进度,达标自动完成+发奖
- * taskKey 匹配约定:title 精确匹配 OR title 以 "[taskKey]" 前缀匹配(迁移时 task_key 拼入 title)
- */
- @Transactional
- public void updateProgress(Long userId, String taskKey, int increment) {
- updateProgress(userId, null, taskKey, increment);
- }
- /**
- * 统一任务系统:更新每日进度型任务的进度(带家庭成员精确定位)
- * 按 (familyMemberId 或 userId→familyMemberId, taskKey) 找当日任务实例,累计进度,达标自动完成+发奖
- * taskKey 匹配约定:title 精确匹配 OR title 以 "[taskKey]" 前缀匹配(迁移时 task_key 拼入 title)
- *
- * 打卡类调用必须显式传 familyMemberId:同一 userId 可能绑定多个家庭成员(多家庭场景),
- * 仅按 user_id 反查(LIMIT 1 无排序)会命中错误成员,导致进度记到他人任务、本人任务永久 pending。
- */
- @Transactional
- public void updateProgress(Long userId, Long familyMemberId, String taskKey, int increment) {
- if (userId == null) {
- // 未绑定登录账号的家庭成员(如仅建档的孩子)无成长任务进度
- return;
- }
- FamilyMember member;
- if (familyMemberId != null) {
- member = familyMemberMapper.selectById(familyMemberId);
- if (member == null || !Long.valueOf(userId).equals(member.getUserId())) {
- log.warn("统一任务进度更新:成员{}不存在或不属于用户{},跳过 taskKey={}",
- familyMemberId, userId, taskKey);
- return;
- }
- } else {
- member = familyMemberMapper.selectOne(
- new LambdaQueryWrapper<FamilyMember>()
- .eq(FamilyMember::getUserId, userId)
- .last("LIMIT 1"));
- }
- if (member == null) {
- return;
- }
- 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);
- Date startOfDay = cal.getTime();
- cal.add(Calendar.DAY_OF_MONTH, 1);
- Date endOfDay = cal.getTime();
- Task task = taskMapper.selectOne(new LambdaQueryWrapper<Task>()
- .eq(Task::getFamilyMemberId, member.getId())
- .eq(Task::getIsDailyProgress, 1)
- .ge(Task::getCreatedAt, startOfDay)
- .lt(Task::getCreatedAt, endOfDay)
- .and(w -> w.eq(Task::getTitle, taskKey)
- .or().likeRight(Task::getTitle, "[" + taskKey + "]"))
- .last("LIMIT 1"));
- if (task == null) {
- log.warn("统一任务进度更新:未找到今日进度型任务 taskKey={}, userId={}", taskKey, userId);
- return;
- }
- if ("completed".equals(task.getStatus())) {
- return;
- }
- // 读当前进度(task_executions.extra.progress),累加
- TaskExecution exec = taskExecutionMapper.selectOne(new LambdaQueryWrapper<TaskExecution>()
- .eq(TaskExecution::getTaskId, task.getId())
- .eq(TaskExecution::getMemberId, member.getId())
- .last("LIMIT 1"));
- int progress = 0;
- if (exec != null && exec.getExtra() != null) {
- try {
- progress = JSON.parseObject(exec.getExtra()).getIntValue("progress");
- } catch (Exception ignore) {
- }
- }
- progress += increment;
- int target = task.getTargetValue() != null ? task.getTargetValue() : 1;
- if (exec == null) {
- exec = new TaskExecution();
- exec.setTaskId(task.getId());
- exec.setMemberId(member.getId());
- exec.setStatus("started");
- exec.setStartedAt(new Date());
- exec.setCreatedAt(new Date());
- }
- exec.setStatus("finished");
- exec.setFinishedAt(new Date());
- exec.setExtra("{\"progress\":" + progress + ",\"target\":" + target + "}");
- if (exec.getId() == null) {
- taskExecutionMapper.insert(exec);
- } else {
- taskExecutionMapper.updateById(exec);
- }
- // 达标自动完成 + 发奖(复用现有奖励链路)
- if (progress >= target) {
- awardTaskCompletionRewards(task, member.getId(), new Date());
- }
- }
- /**
- * 统一任务系统:AI 对话生成任务(原 growth_task createDynamicTask)
- * 直接写入 tasks 表,source_type=ai
- */
- @Transactional
- public Long createDynamicTask(Long userId, String title, String description,
- String dimension, Integer rewardPoints, String sourceConversationId) {
- User user = userService.getUserInfo(userId);
- Task task = new Task();
- task.setFamilyId(user.getFamilyId());
- task.setCreatorId(userId);
- task.setExecutorType("child");
- // 执行者:AI 任务发给当前用户对应家庭成员(若有)
- FamilyMember member = familyMemberMapper.selectOne(
- new LambdaQueryWrapper<FamilyMember>()
- .eq(FamilyMember::getUserId, userId)
- .last("LIMIT 1"));
- if (member != null) {
- task.setExecutorId(member.getId());
- task.setFamilyMemberId(member.getId());
- task.setChildId(member.getId());
- }
- task.setTitle(title);
- task.setDescription(description);
- task.setPoints(rewardPoints != null ? rewardPoints : 10);
- task.setCategory("成长");
- task.setStatus("pending");
- task.setNeedReview(0);
- task.setIsTemplate(0);
- task.setSourceType("ai");
- task.setSourceId(0L);
- task.setDimensionCode(dimension != null ? dimension : "body");
- task.setMemberOnly(0);
- task.setDeadline(new Date(System.currentTimeMillis() + 7L * 24 * 3600 * 1000)); // 7天有效期
- task.setCreatedAt(new Date());
- task.setUpdatedAt(new Date());
- taskMapper.insert(task);
- log.info("创建AI动态任务: userId={}, taskId={}, title={}", userId, task.getId(), title);
- return task.getId();
- }
- /**
- * 统一任务系统:查询用户每日进度型任务列表(原 growth-task/list)
- * type: DAILY(NEWBIE 在 Task 5 处理)
- */
- public List<Map<String, Object>> getGrowthTaskList(Long userId, String type, String dimension) {
- FamilyMember member = familyMemberMapper.selectOne(
- new LambdaQueryWrapper<FamilyMember>()
- .eq(FamilyMember::getUserId, userId)
- .last("LIMIT 1"));
- List<Map<String, Object>> result = new ArrayList<>();
- if (member == null || !"DAILY".equals(type)) {
- return result;
- }
- 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);
- Date startOfDay = cal.getTime();
- cal.add(Calendar.DAY_OF_MONTH, 1);
- Date endOfDay = cal.getTime();
- List<Task> tasks = taskMapper.selectList(new LambdaQueryWrapper<Task>()
- .eq(Task::getFamilyMemberId, member.getId())
- .eq(Task::getIsDailyProgress, 1)
- .ge(Task::getCreatedAt, startOfDay)
- .lt(Task::getCreatedAt, endOfDay)
- .orderByAsc(Task::getId));
- for (Task task : tasks) {
- Map<String, Object> item = new LinkedHashMap<>();
- item.put("taskLogId", task.getId());
- item.put("id", task.getId());
- item.put("type", "DAILY");
- item.put("title", task.getTitle());
- item.put("description", task.getDescription());
- item.put("rewardPoints", task.getPoints());
- item.put("rewardEnergy", 0);
- item.put("targetValue", task.getTargetValue() != null ? task.getTargetValue() : 1);
- item.put("dimension", task.getDimensionCode());
- TaskExecution exec = taskExecutionMapper.selectOne(new LambdaQueryWrapper<TaskExecution>()
- .eq(TaskExecution::getTaskId, task.getId())
- .eq(TaskExecution::getMemberId, member.getId())
- .last("LIMIT 1"));
- int progress = 0;
- if (exec != null && exec.getExtra() != null) {
- try {
- progress = JSON.parseObject(exec.getExtra()).getIntValue("progress");
- } catch (Exception ignore) {
- }
- }
- item.put("progress", progress);
- item.put("completed", "completed".equals(task.getStatus()) ? 1 : 0);
- item.put("claimed", 0); // 统一任务系统:自动发放,无 claimed
- result.add(item);
- }
- return result;
- }
- // ==================== 统一任务系统:新人引导迁移(原 OnboardingService) ====================
- /**
- * 统一任务系统:新用户注册初始化新人引导任务
- * 按 admin_task_templates 中 onboarding_sequence 有值的模板生成 tasks(source_type=onboarding)
- * 顺序链:sequence 递增,前序未完成则后序 locked(复用 prerequisite_task_id + locked 状态)
- */
- @Transactional
- public void initOnboardingTasks(Long userId) {
- User user = userService.getUserInfo(userId);
- if (user == null || user.getFamilyId() == null) {
- return;
- }
- // 幂等:该用户已有 onboarding 任务则跳过
- Long existing = taskMapper.selectCount(new LambdaQueryWrapper<Task>()
- .eq(Task::getCreatorId, userId)
- .eq(Task::getSourceType, "onboarding"));
- if (existing != null && existing > 0) {
- return;
- }
- List<TaskTemplate> templates = taskTemplateMapper.selectList(
- new LambdaQueryWrapper<TaskTemplate>()
- .eq(TaskTemplate::getIsActive, 1)
- .isNotNull(TaskTemplate::getOnboardingSequence)
- .orderByAsc(TaskTemplate::getOnboardingSequence));
- if (templates.isEmpty()) {
- return;
- }
- FamilyMember member = familyMemberMapper.selectOne(
- new LambdaQueryWrapper<FamilyMember>()
- .eq(FamilyMember::getUserId, userId)
- .last("LIMIT 1"));
- Date now = new Date();
- Long prevTaskId = null;
- for (TaskTemplate tpl : templates) {
- Task task = new Task();
- task.setFamilyId(user.getFamilyId());
- task.setCreatorId(userId);
- task.setExecutorType("child");
- if (member != null) {
- task.setExecutorId(member.getId());
- task.setFamilyMemberId(member.getId());
- task.setChildId(member.getId());
- }
- task.setTitle(tpl.getTitle());
- task.setDescription(tpl.getDescription());
- task.setPoints(tpl.getPoints() != null ? tpl.getPoints() : 2);
- task.setCategory(tpl.getCategory());
- task.setStatus(prevTaskId == null ? "pending" : "locked"); // 首任务可直接做,后续需解锁
- task.setNeedReview(tpl.getNeedReview() != null ? tpl.getNeedReview() : 0);
- task.setIsTemplate(0);
- task.setSourceType("onboarding");
- task.setTemplateId(tpl.getId());
- task.setPrerequisiteTaskId(prevTaskId); // 顺序链
- task.setMemberOnly(0);
- task.setDeadline(new Date(now.getTime() + 30L * 24 * 3600 * 1000)); // 30天有效期
- task.setCreatedAt(now);
- task.setUpdatedAt(now);
- taskMapper.insert(task);
- prevTaskId = task.getId();
- }
- log.info("新用户{}初始化{}条新人引导任务", userId, templates.size());
- }
- /**
- * 统一任务系统:完成指定新人引导任务(原 OnboardingService.completeTask)
- * 按 source_type=onboarding + title 精确匹配(title 约定 = 原 taskType,如 FIRST_TASK)
- */
- public void completeOnboardingTask(Long userId, String templateTitle) {
- FamilyMember member = familyMemberMapper.selectOne(
- new LambdaQueryWrapper<FamilyMember>()
- .eq(FamilyMember::getUserId, userId)
- .last("LIMIT 1"));
- if (member == null) {
- return;
- }
- Task task = taskMapper.selectOne(new LambdaQueryWrapper<Task>()
- .eq(Task::getFamilyMemberId, member.getId())
- .eq(Task::getSourceType, "onboarding")
- .eq(Task::getTitle, templateTitle)
- .in(Task::getStatus, "pending", "locked")
- .last("LIMIT 1"));
- if (task != null) {
- awardTaskCompletionRewards(task, member.getId(), new Date());
- unlockPrerequisiteTasks(task.getId());
- }
- }
- /**
- * 统一任务系统:查询用户的新人引导任务列表(原 OnboardingService.getProgress)
- * 返回前端兼容结构:taskType=title、status(completed→CLAIMED,其余→PENDING)、rewardPoints、rewardEnergy
- */
- public List<Map<String, Object>> getOnboardingTasks(Long userId) {
- FamilyMember member = familyMemberMapper.selectOne(
- new LambdaQueryWrapper<FamilyMember>()
- .eq(FamilyMember::getUserId, userId)
- .last("LIMIT 1"));
- List<Map<String, Object>> result = new ArrayList<>();
- if (member == null) {
- return result;
- }
- List<Task> tasks = taskMapper.selectList(new LambdaQueryWrapper<Task>()
- .eq(Task::getFamilyMemberId, member.getId())
- .eq(Task::getSourceType, "onboarding")
- .orderByAsc(Task::getId));
- for (Task task : tasks) {
- Map<String, Object> item = new LinkedHashMap<>();
- item.put("taskType", task.getTitle());
- item.put("rewardPoints", task.getPoints() != null ? task.getPoints() : 0);
- item.put("rewardEnergy", task.getPoints() != null ? task.getPoints() : 0); // 能量≈积分(与 awardTaskCompletionRewards 一致)
- // 状态映射:completed→CLAIMED(奖励已自动发放),其余→PENDING
- item.put("status", "completed".equals(task.getStatus()) ? "CLAIMED" : "PENDING");
- result.add(item);
- }
- return result;
- }
- /**
- * 获取家庭成员可关联的任务列表(含所有状态),供创建任务时选择前置/后置任务
- */
- public List<Task> getRelatedTaskList(Long familyId, Long memberId, String titleKeyword) {
- LambdaQueryWrapper<Task> wrapper = new LambdaQueryWrapper<Task>()
- .eq(Task::getFamilyId, familyId)
- .ne(Task::getStatus, "cancelled")
- .orderByDesc(Task::getCreatedAt);
- if (memberId != null) {
- wrapper.eq(Task::getFamilyMemberId, memberId);
- }
- if (titleKeyword != null && !titleKeyword.trim().isEmpty()) {
- wrapper.like(Task::getTitle, titleKeyword.trim());
- }
- return taskMapper.selectList(wrapper);
- }
- }
|