| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055 |
- 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.CreateTaskDTO;
- import com.etotem.cfc.dto.TaskReviewDTO;
- import com.etotem.cfc.entity.*;
- import com.etotem.cfc.mapper.*;
- import com.etotem.cfc.service.api.TaskServiceInterface;
- import javax.annotation.Resource;
- import lombok.extern.slf4j.Slf4j;
- import org.springframework.stereotype.Service;
- import org.springframework.transaction.annotation.Transactional;
- import java.util.*;
- import com.etotem.cfc.util.SortUtil;
- @Slf4j
- @Service
- public class TaskService implements TaskServiceInterface {
- @Resource
- private TaskMapper taskMapper;
- @Resource
- private FamilyMemberMapper familyMemberMapper;
- @Resource
- private UserMapper userMapper;
- @Resource
- private PointsLogMapper pointsLogMapper;
- @Resource
- private UserService userService;
- @Resource
- private StreakService streakService;
- @Resource
- private MiniGameService miniGameService;
- @Resource
- private MiniGameMapper miniGameMapper;
- @Resource
- private EnergyService energyService;
- @Resource
- private OnboardingService onboardingService;
- @Resource
- private FamilyRelationshipScoreMapper familyRelationshipScoreMapper;
- @Resource
- private GrowthTaskService growthTaskService;
- 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);
- // 为其他孩子创建任务实例
- for (int i = 1; i < childIds.size(); i++) {
- Task additionalTask = createSingleTask(currentUser, userId, executorType, childIds.get(i), dto);
- taskMapper.insert(additionalTask);
- }
- return task.getId();
- } else {
- // 单个孩子或无孩子
- Long memberId = childIds.isEmpty() ? null : childIds.get(0);
- Task task = createSingleTask(currentUser, userId, executorType, memberId, dto);
- taskMapper.insert(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.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.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);
- 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::getChildId, memberId)
- .ge(Task::getDeadline, startOfDay)
- .lt(Task::getDeadline, endOfDay)
- .in(Task::getStatus, Arrays.asList("pending", "completed"))
- .orderByAsc(Task::getDeadline);
- if (dimensionCode != null && !dimensionCode.isEmpty()) {
- wrapper.and(w -> w
- .apply("COALESCE(JSON_EXTRACT(dimension_weights, CONCAT('$.', {0})), 0) > 0", dimensionCode)
- .or().eq(Task::getDimensionCode, dimensionCode)
- );
- }
- SortUtil.applySort(wrapper);
- return taskMapper.selectList(wrapper);
- }
- /**
- * 按维度分类获取今日任务(五维页面筛选用)
- */
- 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 taskMapper.selectList(new LambdaQueryWrapper<Task>()
- .eq(Task::getChildId, memberId)
- .eq(Task::getCategory, category)
- .ge(Task::getDeadline, startOfDay)
- .lt(Task::getDeadline, endOfDay)
- .in(Task::getStatus, Arrays.asList("pending", "completed"))
- .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::getChildId, 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::getChildId, 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;
- }
- @Transactional
- public Map<String, Object> completeTask(Long taskId, Long memberId, String photoUrl) {
- Task task = taskMapper.selectById(taskId);
- if (task == null || !task.getChildId().equals(memberId)) {
- return null;
- }
- if ("completed".equals(task.getStatus())) {
- return null; // 任务已完成,不能重复提交
- }
- Date now = new Date();
- int pointsEarned;
- // 检查是否为小游戏任务
- if ("小游戏类".equals(task.getCategory()) && task.getMinigameCode() != null) {
- // 小游戏任务不通过此接口完成,需要通过小游戏界面完成
- // 这里只返回null,实际完成逻辑在completeMinigameTask方法中
- return null;
- }
- pointsEarned = task.getPoints();
- boolean isEarly = now.before(task.getDeadline());
- boolean isLateGrace = false;
- // 检查是否在宽限期内
- if (!isEarly) {
- long diffMinutes = (now.getTime() - task.getDeadline().getTime()) / (1000 * 60);
- isLateGrace = diffMinutes <= LATE_GRACE_MINUTES;
- }
- FamilyMember child = familyMemberMapper.selectById(memberId);
- // POINT-002: 计算积分(支持自定义超额规则)
- String bonusRuleJson = task.getBonusRule();
- if (bonusRuleJson != null && !bonusRuleJson.isEmpty()) {
- // 解析自定义超额积分规则
- pointsEarned = calculatePointsWithBonusRule(task, now, isEarly, isLateGrace, child);
- } else {
- // 使用默认规则
- if (isEarly || isLateGrace) {
- // 提前完成或宽限期内完成,获得基础分 + 提前奖励
- pointsEarned += EARLY_BONUS;
- } else {
- // 超时,检查是否开启扣分
- pointsEarned = calculateLatePenalty(task, child, memberId);
- }
- }
- // 更新任务状态
- task.setStatus("completed");
- task.setCompletedAt(now);
- 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 (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);
- }
- // 记录积分流水
- 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());
- // POINT-009: 设置积分过期时间
- 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 { growthTaskService.updateProgress(child.getUserId(), "DAILY_TASK", 1); } catch (Exception e) { log.warn("成长任务进度更新失败: memberId={}, error={}", memberId, e.getMessage()); }
- Map<String, Object> result = new HashMap<>();
- result.put("pointsEarned", pointsEarned);
- result.put("newBalance", child.getTotalPoints());
- result.put("needReview", task.getNeedReview() == 1);
- return result;
- }
- public boolean reviewTask(Long taskId, TaskReviewDTO dto) {
- Task task = taskMapper.selectById(taskId);
- if (task == null) {
- return false;
- }
- // 兼容两种审核字段:approved 和 result
- boolean isApproved;
- if (dto.getApproved() != null) {
- // 使用 approved 字段(推荐)
- isApproved = dto.getApproved();
- } else if ("pass".equals(dto.getResult())) {
- // 兼容 result 字段
- isApproved = true;
- } else {
- isApproved = false;
- }
- // 更新任务状态
- if (isApproved) {
- task.setStatus("completed");
- } else {
- task.setStatus("pending"); // 审核不通过,任务重置
- }
- 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::getChildId, 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::getChildId, 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, "completed")
- .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);
- // 更新家长积分
- 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.getChildId());
- 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.getChildId()); if (child != null) { onboardingService.completeTask(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);
- 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 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::getChildId, memberId)
- .eq(Task::getNeedReview, 1)
- .eq(Task::getStatus, "completed")
- .orderByDesc(Task::getCompletedAt));
- }
- /**
- * 获取孩子的所有任务
- */
- public List<Task> getAllTasksByChild(Long memberId) {
- return taskMapper.selectList(new LambdaQueryWrapper<Task>()
- .eq(Task::getChildId, 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::getChildId, 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();
- 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 0; // 达到每日扣分上限
- }
- } else {
- return 0; // 扣分未开启
- }
- }
- /**
- * 完成小游戏任务
- * @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 || !task.getChildId().equals(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)
- .in(Task::getStatus, Arrays.asList("completed", "pending"))
- .orderByDesc(Task::getCreatedAt));
- }
- /**
- * 获取指定家庭成员的全部任务
- */
- 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);
- }
- }
|